diff --git a/.gitignore b/.gitignore index fa6cbb454b..773b4726c0 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,4 @@ prime/ *.snap *.snap-build *_source.tar.bz2 +.DS_Store \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 9c78a83451..fd7393372b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,9 @@ linters: disable-all: true fast: false +run: + timeout: 3m + linters-settings: gocritic: disabled-checks: diff --git a/CHANGELOG.md b/CHANGELOG.md index 540b1a9790..2a88eb1035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,291 @@ This changelog goes through all the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.io). +## [1.10.0-RC1](https://github.com/go-gitea/gitea/releases/tag/v1.10.0-rc1) - 2019-10-14 +* BREAKING + * Remove legacy handling of drone token (#8191) + * Change repo search to use exact match for topic search. (#7941) + * Add pagination for admin api get orgs and fix only list public orgs bug (#7742) + * Implement the ability to change the ssh port to match what is in the gitea config (#7286) +* FEATURE + * Org/Members: display 2FA members states + optimize sql requests (#7621) + * SetDefaultBranch on pushing to empty repository (#7610) + * Adds side-by-side diff for images (#6784) + * API method to list all commits of a repository (#6408) + * Password Complexity Checks (#6230) + * Add option to initialize repository with labels (#6061) + * Add additional password hash algorithms (#6023) +* BUGFIXES + * Fix errors in create org UI regarding team access permission (#8506) + * Fix bug on FindExternalUsersByProvider (#8504) + * Create .ssh dir as necessary (#8486) + * IsBranchExist: return false if provided name is empty (#8485) + * Making openssh listen on SSH_LISTEN_PORT not SSH_PORT (#8477) + * Add check for empty set when dropping indexes during migration (#8471) + * LFS files are relative to LFS content path, ensure that when deleting they are made relative to this (#8455) + * Ensure Request Body Readers are closed in LFS server (#8454) + * Fix template bug on mirror repository setting page (#8438) + * Fix migration v96 to keep issue attachments (#8435) + * Update strk.kbt.io/projects/go/libravatar to latest (#8429) + * Singular form for files that has only one line (#8416) + * Check for either escaped or unescaped wiki filenames (#8408) + * Allow users with explicit read access to give approvals (#8382) + * Fix editor commit to new branch if PR disabled (#8375) + * readd .markdown class to all markup renderers (#8357) + * Upgrade xorm to v0.7.9 to fix some bugs (#8354) + * Fix column name ambiguity in GetUserIssueStats() (#8347) + * Change general form binding to gogs form (#8334) + * Fix pull request commit status in user dashboard list (#8321) + * Fix repo_admin_change_team_access always checked in org settings (#8319) + * Update to github.com/lafriks/xormstore@v1.3.0 (#8317) + * Show correct commit status in PR list (#8316) + * Bugfix for image compare and minor improvements to image compare (#8289) + * Update xorm (#8286) + * Fix API for edit and delete release attachment (#8285) + * Fix nil object access in some conditions when parsing cross references (#8281) + * Fix label count (#8267) + * Only show teams access for organization repositories on collaboration setting page (#8265) + * Test more reserved usernames (#8263) + * Rewrite reference processing code in preparation for opening/closing from comment references (#8261) + * Fix assets key on release webhook (#8253) + * Allow registration when button is hidden (#8237) + * Fix release API URL generation (#8234) + * Fix milestone num_issues (#8221) + * MS Teams webhook misses commit messages (#8209) + * Fix data race (#8204) + * Fix team user api (#8172) + * Fix pull merge 500 error caused by git-fetch breaking behaviors (#8161) + * Make show private icon when repo avatar set (#8144) + * Add reviewers as participants (#8121) + * Fix Go 1.13 private repository go get issue (#8112) + * feat: highlight issue references with : (#8101) + * Make AllowedUsers configurable in sshd_config (#8094) + * Strict name matching for Repository.GetTagID() (#8074) + * Avoid ambiguity of branch/directory names for the git-diff-tree command (#8066) + * Add change title notification for issues (#8061) + * [ssh] fix the config specification in the authorized_keys template (#8031) + * Fix reading git notes from nested trees (#8026) + * Fixes synchronize tags to releases for repository - makes sure we are only getting tag refs (#7990) + * Fix adding default Telegram webhook (#7972) + * Run CORS handler first for /api routes (#7967) + * Abort synchronization from LDAP source if there is some error. (#7960) + * Fix wrong sender when send slack webhook (#7918) + * Fix bug when migrating a private repository (#7917) + * Evaluate emojis in commit messages in list view (#7906) + * Fix upload file type check (#7890) + * lfs/lock: round locked_at timestamp to second (#7872) + * fix non existent milestone with 500 error instead of 404 (#7867) + * gpg/bugfix: Use .ExpiredUnix.IsZero to display green color of forever valid gpg key (#7846) + * Fix duplicate call of webhook (#7821) + * Enable switching to a different source branch when PR already exists (#7819) + * Convert files to utf-8 for indexing (#7814) + * Do not fetch all refs in pull-request compare (#7797) + * Fix multiple bugs with statuses endpoints at API (#7785) + * Restore functionality for early gits (#7775) + * Fix Slack webhook fork message (#7774) + * Rewrite existing repo units if setting is not included in api body (#7763) + * Fix rename failed when rewrite public keys (#7761) + * Fix approvals counting (#7757) + * Add migration step to remove old repo_indexer_status orphaned records (#7746) + * Fix repo_index_status lingering when deleting a repository (#7734) + * Remove camel case tokenization from repo indexer (#7733) + * Fix milestone completness calculation when migrating (#7725) + * Regression: Include "executable" files in the index, as they are not necessarily … (#7718) + * Fixes indexed repos keeping outdated indexes when files grow too large (#7712) + * Skip non-regular files (e.g. submodules) on repo indexing (#7711) + * Fix dropTableColumns sqlite implementation (#7710) + * Update gopkg.in/src-d/go-git.v4 to v4.13.1 (#7705) + * improve branches list performance and fix protected branch icon when no-login (#7695) + * Correct wrong datetime format for git (#7689) + * Move add to hook queue for created repo to outside xorm session. (#7675) + * sugestion to use range .Branches (#7674) + * Fix bug on migrating milestone from github (#7665) + * hide delete/restore button on archived repos (#7658) + * css: use flex to fix floating paginate (#7656) + * Fix syntax highlight initialization (#7617) + * Fix panic on push at - Merging pull request causes 500 error (#7615) + * Make PKCS8, PEM and SSH2 keys work (#7600) + * Fix mistake in arc-green.less split-diff css code. (#7587) + * Handle ErrUserProhibitLogin in http git (#7586) + * Fix bug create/edit wiki pages when code master branch protected (#7580) + * Fixes Malformed URLs in API git/commits response (#7565) + * Fix file header overflow in file and blame views (#7562) + * Improve SSH key parser to handle newlines in keys (#7522) + * Fix empty commits now showing in repo overview (#7521) + * Fix repository's pull request count error (#7518) + * Fix markdown invoke sequence (#7513) + * Remove duplicated webhook trigger (#7511) + * Update User.NumRepos atomically in createRepository (#7493) + * Fix settings page of repo you aren't admin print error - Settings pages giving UnitType error message (#7482) + * Fix redirection after file edit - Handles all redirects for Web UI File CRUD (#7478) + * cmd/serv: actually exit after fatal errors (#7458) + * Fix an issue with some pages throwing 'not defined' js exceptions (#7450) + * fix Dropzone.js integration (#7445) + * Fix regex for issues in commit messages (#7444) + * Diff: Fix indentation on unhighlighted code (#7435) + * Only show "New Pull Request" button if repo allows pulls (#7426) + * Upgrade macaron/captcha to fix random error problem (#7407) + * create class for inline positioned lists (#7393) + * Fetch refs for successful testing for tag (#7388) + * add missing template variable on organisation settings (#7385) + * fix post parameter - on issue list - unset assignee (#7380) + * fix/define autochecked checkboxes on issue list in firefox (#7320) + * only return head: null if source branch was deleted (#6705) +* ENHANCEMENT + * Add nofollow to sign in links (#8509) + * vendor: update mvdan.cc/xurls/v2 to v2.1.0 (#8495) + * Update milestone issues numbers when save milestone and other code improvements (#8411) + * Add extra user information when migrating release (#8331) + * Require overall success if no context is given for status check (#8318) + * Transaction-aware retry create issue to cope with duplicate keys (#8307) + * Change link on issue milestone (#8246) + * Alwaywas return local url for users avatar (#8245) + * Move some milestone functions to a standalone package (#8213) + * Move create issue comment to comments package (#8212) + * Disable max height property of comment textarea (#8203) + * Add 'Mentioning you' group to /issues page (#8201) + * oauth2 with remote Gitea (#8149) + * Reference issues from pull requests and other issues (#8137) + * Fix webhooks to use proxy from environment (#8116) + * Add merged commit id on pull view when it's merged (#8062) + * Add teams to repo on collaboration page. (#8045) + * Update swagger to 0.20.1 (#8010) + * Make link last commit massages in repository home page and commit tables (#8006) + * Add API endpoint for accessing repo topics (#7963) + * Include description in repository search (#7942) + * Use gitea forked macaron (#7933) + * Fix pull creation with empty changes (#7920) + * Allow token as authorization for accessing attachments (#7909) + * Retry create issue to cope with duplicate keys (#7898) + * Move git diff codes from models to services/gitdiff (#7889) + * migrate gplus to google oauth2 provider (#7885) + * Remove unique filter from repo indexer analyzer. (#7878) + * Detect delimiter in CSV rendering (#7869) + * Import topics during migration (#7851) + * Move CreateReview to modules/pull (#7841) + * vendor: update pdf.js to v2.1.266 (#7834) + * Support SSH_LISTEN_PORT env var in docker app.ini template (#7829) + * Add Ability for User to Customize Email Notification Frequency (#7813) + * Move database settings from models to setting (#7806) + * Display ui time with customize time location (#7792) + * Implement webhook branch filter (#7791) + * Restrict repository indexing by glob match (#7767) + * Api: advanced settings for repository (external wiki, issue tracker etc.) (#7756) + * Update migrated repositories' issues/comments/prs poster id if user has a github external user saved (#7751) + * deps: Upgrade gopkg.in/editorconfig/editorconfig-core-go.v1 (#7749) + * Apply emoji on commit graph page (#7743) + * Add a lot of extension to language mappings for syntax highlights (#7741) + * Add SQL execution on log and indexes on table repository and comment (#7740) + * Set DB connection error level to error (#7724) + * Check commit message hashes before making links (#7713) + * remove unnecessary fmt on generate bindata (#7706) + * Fix specific highlighting (CMakeLists.txt ...) (#7686) + * Add file status on API (#7671) + * Add support for DEFAULT_ORG_MEMBER_VISIBLE (#7669) + * Provide links in commit summaries in commits table/view list (#7659) + * Change length of some repository's columns (#7652) + * Move commit repo action from models to repofiles package (#7645) + * fix wrong email when use gitea as OAuth2 provider (#7640) + * [Branch View] add download button (#7604) + * Update to xorm@v0.7.4 (#7596) + * use 403 instead of 401 for ErrUserProhibitLogin (#7591) + * Removed unnecessary conversions (#7557) + * Un-lambda base.FileSize (#7556) + * Added missing error checks in tests (#7554) + * Move create release from models to a standalone package (#7539) + * Make default branch name link to default branch (#7519) + * Added total count of contributions to heatmap (#7517) + * Move mirror to a standalone package from models (#7486) + * Move models.PushUpdate to repofiles.PushUpdate (#7485) + * Include thread related headers in issue/coment mail (#7484) + * Refuse merge until all required status checks success (#7481) + * convert all js var to let/const (#7464) + * Only create branches for opened pull requestes when migrating from github (#7463) + * jQuery 3 (#7425) + * Add notification placeholder (#7409) + * Search Commits via Commit Hash (#7400) + * Move status table to cron package (#7370) + * wiki - page revisions list (#7369) + * Display original author and URL information when showing migrated issues/comments (#7352) + * Refactor filetype is not allowed errors (#7309) + * switch to use gliderlabs/ssh for builtin server (#7250) + * Remove settting dependency on modules/session (#7237) + * Move all mail related codes from models to services/mailer (#7200) + * Support git.PATH entry in app.ini (#6772) + * Support setting cookie domain (#6288) + * Move migrating repository from frontend to backend (#6200) + * Delete releases attachments if release is deleted (#6068) +* SECURITY + * Ignore mentions for users with no access (#8395) + * Be more strict with git arguments (#7715) + * reserve .well-known username (#7637) +* TRANSLATION + * Latvian translation for home page (#8468) + * Add home template italian translation (#8352) + * fix misprint (#7452) +* BUILD + * use go 1.13 (#8088) +* MISC + * add file line count info on UI (#8396) + * Make issues page left menu 100% width and add reponame as title attribute (#8359) + * [arc-green] white on hover for active menu items (#8344) + * Move ref (branch or tag) location on issue list page (#8157) + * apply emoji on dashboard issue list labels (#8156) + * 1148: Take up the full width when viewing the diff in split view. (#8114) + * Display description of 'make this repo private' as help text, not as tooltip (#8097) + * Fixes deformed emoji in pull request reviews (#8047) + * Add strike to old header on comment (#8046) + * Add tooltip for the visibility checkbox in /repo/create (#8025) + * Update github.com/lafriks/xormstore and tidy up mod.go (#8020) + * keep blame view buttons sequence consistent with normal view when view a file (#8007) + * Use "Pull Request" instead of "Merge Request" (#8003) + * Move line number to :before attr to hide from search on browser (#8002) + * Changed black color to white for (read) number label on issue list page (#8000) + * [Branch View] show "New Pull Request" Button only if posible (#7977) + * Fix hook problem by only setting the git environment variables if we are passed them (#7854) + * Prevent Commit Status and Message From Overflowing On Branch Page (#7800) + * Fix global search result CSS, misc CSS tweaks (#7789) + * Tweak label border CSS (#7739) + * Fix create menu item widths (#7708) + * Extract the username and password from the mirror url (#7651) + * [Branch View] Delete duplicate protection symbol (#7624) + * [Branch View] Delete Table Header (#7622) + * [Branch View] icons to buttons (#7602) + * update js dependencies (#7462) + * Add Extra Info to Branches Page (#7461) + * Bump lodash from 4.17.11 to 4.17.14 (#7459) + * wiki history improvements (#7391) + * ui fixes - compare view and archieved repo issues (#7345) + * dark theme scrollbars (#7269) + * wiki - editor - add buttons 'inline code', 'empty checkbox', 'checked checkbox' (#7243) + * Fix Statuses API only shows first 10 statuses: Add paging and extend API GetCommitStatuses (#7141) + +## [1.9.4](https://github.com/go-gitea/gitea/releases/tag/v1.9.4) - 2019-10-08 +* BUGFIXES + * Highlight issue references (#8101) (#8404) + * Fix bug when migrating a private repository #7917 (#8403) + * Change general form binding to gogs form (#8334) (#8402) + * Fix editor commit to new branch if PR disabled (#8375) (#8401) + * Fix milestone num_issues (#8221) (#8400) + * Allow users with explicit read access to give approvals (#8398) + * Fix commit status in PR #8316 and PR #8321 (#8339) + * Fix API for edit and delete release attachment (#8290) + * Fix assets on release webhook (#8283) + * Fix release API URL generation (#8239) + * Allow registration when button is hidden (#8238) + * MS Teams webhook misses commit messages (backport v1.9) (#8225) + * Fix data race (#8206) + * Fix pull merge 500 error caused by git-fetch breaking behaviors (#8194) + * Fix the SSH config specification in the authorized_keys template (#8193) + * Fix reading git notes from nested trees (#8189) + * Fix team user api (#8172) (#8188) + * Add reviewers as participants (#8124) +* BUILD + * Use vendored go-swagger (#8087) (#8165) + * Fix version-validation for GO 1.13 (go-macaron/cors) (#8389) +* MISC + * Make show private icon when repo avatar set (#8144) (#8175) + ## [1.9.3](https://github.com/go-gitea/gitea/releases/tag/v1.9.3) - 2019-09-06 * BUGFIXES * Fix go get from a private repository with Go 1.13 (#8100) @@ -96,20 +381,20 @@ been added to each release, please refer to the [blog](https://blog.gitea.io). * Move add to hook queue for created repo to outside xorm session. (#7682) (#7675) * Show protection symbol if needed on default branch (#7660) (#7668) * Hide delete/restore button on archived repos (#7660) - * Fix bug on migrating milestone from github (#7665) (#7666) + * Fix bug on migrating milestone from github (#7665) (#7666) * Use flex to fix floating paginate (#7656) (#7662) * Change length of some repository's columns (#7652) (#7655) * Fix wrong email when use gitea as OAuth2 provider (#7640) (#7647) - * Fix syntax highlight initialization (#7617) (#7626) + * Fix syntax highlight initialization (#7617) (#7626) * Fix bug create/edit wiki pages when code master branch protected (#7580) (#7623) * Fix panic on push at #7611 (#7615) (#7618) - * Handle ErrUserProhibitLogin in http git (#7586, #7591) (#7590) + * Handle ErrUserProhibitLogin in http git (#7586, #7591) (#7590) * Fix color of split-diff view in dark theme (#7587) (#7589) - * Fix file header overflow in file and blame views (#7562) (#7579) + * Fix file header overflow in file and blame views (#7562) (#7579) * Malformed URLs in API git/commits response (#7565) (#7567) * Fix empty commits now showing in repo overview (#7521) (#7563) - * Fix repository's pull request count error (#7518) (#7524) - * Remove duplicated webhook trigger (#7511) (#7516) + * Fix repository's pull request count error (#7518) (#7524) + * Remove duplicated webhook trigger (#7511) (#7516) * Handles all redirects for Web UI File CRUD (#7478) (#7507) * Fix regex for issues in commit messages (#7444) (#7466) * cmd/serv: actually exit after fatal errors (#7458) (#7460) @@ -710,7 +995,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.io). ## [1.7.5](https://github.com/go-gitea/gitea/releases/tag/v1.7.5) - 2019-03-27 * BUGFIXES * Fix unitTypeCode not being used in accessLevelUnit (#6419) (#6423) - * Fix bug where manifest.json was being requested without cookies and continuously creating new sessions (#6372) (#6383) + * Fix bug where manifest.json was being requested without cookies and continuously creating new sessions (#6372) (#6383) * Fix ParsePatch function to work with quoted diff --git strings (#6323) (#6332) ## [1.7.4](https://github.com/go-gitea/gitea/releases/tag/v1.7.4) - 2019-03-12 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95799e92b3..04ffebe628 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,7 @@ - [Translation](#translation) - [Code review](#code-review) - [Styleguide](#styleguide) + - [Design guideline](#design-guideline) - [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) - [Release Cycle](#release-cycle) - [Maintainers](#maintainers) @@ -71,13 +72,15 @@ Here's how to run the test suite: - Install the correct version of the drone-cli package. As of this writing, the correct drone-cli version is - [1.1.0](https://docs.drone.io/cli/install/). + [1.2.0](https://docs.drone.io/cli/install/). - Ensure you have enough free disk space. You will need at least 15-20 Gb of free disk space to hold all of the containers drone creates (a default AWS or GCE disk size won't work -- see [#6243](https://github.com/go-gitea/gitea/issues/6243)). - Change into the base directory of your copy of the gitea repository, and run `drone exec --event pull_request`. +- At the moment `drone exec` doesn't support the Docker Toolbox on Windows 10 + (see [drone-cli#135](https://github.com/drone/drone-cli/issues/135)) The drone version, command line, and disk requirements do change over time (see [#4053](https://github.com/go-gitea/gitea/issues/4053) and @@ -118,6 +121,8 @@ An exception are the tools to build the CSS and images. - To build Images: ImageMagick, inkscape and zopflipng binaries must be available in your `PATH` to run `make generate-images`. +For more details on how to generate files, build and test Gitea, see the [hacking instructions](https://docs.gitea.io/en-us/hacking-on-gitea/) + ## Code review Changes to Gitea must be reviewed before they are accepted—no matter who @@ -157,6 +162,22 @@ import ( ) ``` +## Design guideline + +To maintain understandable code and avoid circular dependencies it is important to have a good structure of the code. The gitea code is divided into the following parts: + +- **integration:** Integrations tests +- **models:** Contains the data structures used by xorm to construct database tables. It also contains supporting functions to query and update the database. Dependecies to other code in Gitea should be avoided although some modules might be needed (for example for logging). +- **models/fixtures:** Sample model data used in integration tests. +- **models/migrations:** Handling of database migrations between versions. PRs that changes a database structure shall also have a migration step. +- **modules:** Different modules to handle specific functionality in Gitea. +- **public:** Frontend files (javascript, images, css, etc.) +- **routers:** Handling of server requests. As it uses other Gitea packages to serve the request, other packages (models, modules or services) shall not depend on routers +- **services:** Support functions for common routing operations. Uses models and modules to handle the request. +- **templates:** Golang templates for generating the html output. +- **vendor:** External code that Gitea depends on. + + ## Developer Certificate of Origin (DCO) We consider the act of contributing to the code by submitting a Pull @@ -283,7 +304,7 @@ be reviewed by two maintainers and must pass the automatic tests. * Add a tag as `git tag -s -F release.notes v$vmaj.$vmin.$`, release.notes file could be a temporary file to only include the changelog this version which you added to `CHANGELOG.md`. * And then push the tag as `git push origin v$vmaj.$vmin.$`. Drone CI will automatically created a release and upload all the compiled binary. (But currently it didn't add the release notes automatically. Maybe we should fix that.) * If needed send PR for changelog on branch `master`. -* Send PR to [blog repository](https://github.com/go-gitea/blog) announcing the release. +* Send PR to [blog repository](https://gitea.com/gitea/blog) announcing the release. ## Copyright diff --git a/MAINTAINERS b/MAINTAINERS index bf657fabe2..9d3e4bc848 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -33,3 +33,4 @@ silverwind (@silverwind) Gary Kim (@gary-kim) Guillermo Prandi (@guillep2k) Mura Li (@typeless) +6543 <6543@obermui.de> (@6543) diff --git a/Makefile b/Makefile index b881bc9553..ebcfadb21d 100644 --- a/Makefile +++ b/Makefile @@ -168,6 +168,10 @@ fmt-check: test: GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' $(PACKAGES) +.PHONY: test\#% +test\#%: + GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' -run $* $(PACKAGES) + .PHONY: coverage coverage: @hash gocovmerge > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @@ -515,6 +519,6 @@ pr: golangci-lint: @hash golangci-lint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ export BINARY="golangci-lint"; \ - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.18.0; \ + curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.20.0; \ fi - golangci-lint run --deadline=3m + golangci-lint run diff --git a/README.md b/README.md index 92ed78a497..96f755107a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [简体中文](https://github.com/go-gitea/gitea/blob/master/README_ZH.md) -# Gitea - Git with a cup of tea +

logo Gitea - Git with a cup of tea

[![Build Status](https://drone.gitea.io/api/badges/go-gitea/gitea/status.svg)](https://drone.gitea.io/go-gitea/gitea) [![Join the Discord chat at https://discord.gg/NsatcWJ](https://img.shields.io/discord/322538954119184384.svg)](https://discord.gg/NsatcWJ) @@ -10,8 +10,9 @@ [![GoDoc](https://godoc.org/code.gitea.io/gitea?status.svg)](https://godoc.org/code.gitea.io/gitea) [![GitHub release](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest) [![Help Contribute to Open Source](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea) -[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backer&color=brightgreen)](https://opencollective.com/gitea) +[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) ## Purpose diff --git a/README_ZH.md b/README_ZH.md index e143f23b41..0d9d6d27da 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -9,8 +9,9 @@ [![Go Report Card](https://goreportcard.com/badge/code.gitea.io/gitea)](https://goreportcard.com/report/code.gitea.io/gitea) [![GoDoc](https://godoc.org/code.gitea.io/gitea?status.svg)](https://godoc.org/code.gitea.io/gitea) [![GitHub release](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest) -[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backer&color=brightgreen)](https://opencollective.com/gitea) +[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![Crowdin](https://badges.crowdin.net/gitea/localized.svg)](https://crowdin.com/project/gitea) ## 目标 diff --git a/cmd/admin.go b/cmd/admin.go index 4c4d6f9b66..4346159feb 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -13,9 +13,9 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth/oauth2" - "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + pwd "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/setting" "github.com/urfave/cli" @@ -233,7 +233,9 @@ func runChangePassword(c *cli.Context) error { if err := initDB(); err != nil { return err } - + if !pwd.IsComplexEnough(c.String("password")) { + return errors.New("Password does not meet complexity requirements") + } uname := c.String("username") user, err := models.GetUserByName(uname) if err != nil { @@ -243,6 +245,7 @@ func runChangePassword(c *cli.Context) error { return err } user.HashPassword(c.String("password")) + if err := models.UpdateUserCols(user, "passwd", "salt"); err != nil { return err } @@ -275,26 +278,24 @@ func runCreateUser(c *cli.Context) error { fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n") } - var password string + if err := initDB(); err != nil { + return err + } + var password string if c.IsSet("password") { password = c.String("password") } else if c.IsSet("random-password") { var err error - password, err = generate.GetRandomString(c.Int("random-password-length")) + password, err = pwd.Generate(c.Int("random-password-length")) if err != nil { return err } - fmt.Printf("generated random password is '%s'\n", password) } else { return errors.New("must set either password or random-password flag") } - if err := initDB(); err != nil { - return err - } - // always default to true var changePassword = true diff --git a/cmd/hook.go b/cmd/hook.go index f5b7962aab..f07568dd8b 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -66,6 +66,7 @@ func runHookPreReceive(c *cli.Context) error { reponame := os.Getenv(models.EnvRepoName) userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchPRID), 10, 64) + isDeployKey, _ := strconv.ParseBool(os.Getenv(models.EnvIsDeployKey)) buf := bytes.NewBuffer(nil) scanner := bufio.NewScanner(os.Stdin) @@ -98,6 +99,7 @@ func runHookPreReceive(c *cli.Context) error { GitObjectDirectory: os.Getenv(private.GitObjectDirectory), GitQuarantinePath: os.Getenv(private.GitQuarantinePath), ProtectedBranchID: prID, + IsDeployKey: isDeployKey, }) switch statusCode { case http.StatusInternalServerError: diff --git a/cmd/serv.go b/cmd/serv.go index 6533b0371c..1ac6b21e53 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -191,6 +191,8 @@ func runServ(c *cli.Context) error { os.Setenv(models.EnvPusherID, strconv.FormatInt(results.UserID, 10)) os.Setenv(models.ProtectedBranchRepoID, strconv.FormatInt(results.RepoID, 10)) os.Setenv(models.ProtectedBranchPRID, fmt.Sprintf("%d", 0)) + os.Setenv(models.EnvIsDeployKey, fmt.Sprintf("%t", results.IsDeployKey)) + os.Setenv(models.EnvKeyID, fmt.Sprintf("%d", results.KeyID)) //LFS token authentication if verb == lfsAuthenticateVerb { diff --git a/cmd/web.go b/cmd/web.go index 9a5ce5d2b6..3ca4041a7d 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -13,6 +13,7 @@ import ( "os" "strings" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" @@ -75,17 +76,13 @@ func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler) } go func() { log.Info("Running Let's Encrypt handler on %s", setting.HTTPAddr+":"+setting.PortToRedirect) - var err = http.ListenAndServe(setting.HTTPAddr+":"+setting.PortToRedirect, certManager.HTTPHandler(http.HandlerFunc(runLetsEncryptFallbackHandler))) // all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validation happens here) + // all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validation happens here) + var err = runHTTP(setting.HTTPAddr+":"+setting.PortToRedirect, certManager.HTTPHandler(http.HandlerFunc(runLetsEncryptFallbackHandler))) if err != nil { log.Fatal("Failed to start the Let's Encrypt handler on port %s: %v", setting.PortToRedirect, err) } }() - server := &http.Server{ - Addr: listenAddr, - Handler: m, - TLSConfig: certManager.TLSConfig(), - } - return server.ListenAndServeTLS("", "") + return runHTTPSWithTLSConfig(listenAddr, certManager.TLSConfig(), context2.ClearHandler(m)) } func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) { @@ -101,12 +98,21 @@ func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) { } func runWeb(ctx *cli.Context) error { + if os.Getppid() > 1 && len(os.Getenv("LISTEN_FDS")) > 0 { + log.Info("Restarting Gitea on PID: %d from parent PID: %d", os.Getpid(), os.Getppid()) + } else { + log.Info("Starting Gitea on PID: %d", os.Getpid()) + } + + // Set pid file setting if ctx.IsSet("pid") { setting.CustomPID = ctx.String("pid") } + // Perform global initialization routers.GlobalInit() + // Set up Macaron m := routes.NewMacaron() routes.RegisterRoutes(m) @@ -164,6 +170,7 @@ func runWeb(ctx *cli.Context) error { var err error switch setting.Protocol { case setting.HTTP: + NoHTTPRedirector() err = runHTTP(listenAddr, context2.ClearHandler(m)) case setting.HTTPS: if setting.EnableLetsEncrypt { @@ -172,9 +179,15 @@ func runWeb(ctx *cli.Context) error { } if setting.RedirectOtherPort { go runHTTPRedirector() + } else { + NoHTTPRedirector() } err = runHTTPS(listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m)) case setting.FCGI: + NoHTTPRedirector() + // FCGI listeners are provided as stdin - this is orthogonal to the LISTEN_FDS approach + // in graceful and systemD + NoMainListener() var listener net.Listener listener, err = net.Listen("tcp", listenAddr) if err != nil { @@ -187,6 +200,10 @@ func runWeb(ctx *cli.Context) error { }() err = fcgi.Serve(listener, context2.ClearHandler(m)) case setting.UnixSocket: + // This could potentially be inherited using LISTEN_FDS but currently + // these cannot be inherited + NoHTTPRedirector() + NoMainListener() if err := os.Remove(listenAddr); err != nil && !os.IsNotExist(err) { log.Fatal("Failed to remove unix socket directory %s: %v", listenAddr, err) } @@ -207,8 +224,10 @@ func runWeb(ctx *cli.Context) error { } if err != nil { - log.Fatal("Failed to start server: %v", err) + log.Critical("Failed to start server: %v", err) } - + log.Info("HTTP Listener: %s Closed", listenAddr) + graceful.WaitForServers() + log.Close() return nil } diff --git a/cmd/web_graceful.go b/cmd/web_graceful.go index 53f407ce9e..07b5a964c5 100644 --- a/cmd/web_graceful.go +++ b/cmd/web_graceful.go @@ -10,36 +10,28 @@ import ( "crypto/tls" "net/http" - "code.gitea.io/gitea/modules/log" - - "github.com/facebookgo/grace/gracehttp" + "code.gitea.io/gitea/modules/graceful" ) func runHTTP(listenAddr string, m http.Handler) error { - return gracehttp.Serve(&http.Server{ - Addr: listenAddr, - Handler: m, - }) + return graceful.HTTPListenAndServe("tcp", listenAddr, m) } func runHTTPS(listenAddr, certFile, keyFile string, m http.Handler) error { - config := &tls.Config{ - MinVersion: tls.VersionTLS10, - } - if config.NextProtos == nil { - config.NextProtos = []string{"http/1.1"} - } - - config.Certificates = make([]tls.Certificate, 1) - var err error - config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - log.Fatal("Failed to load https cert file %s: %v", listenAddr, err) - } - - return gracehttp.Serve(&http.Server{ - Addr: listenAddr, - Handler: m, - TLSConfig: config, - }) + return graceful.HTTPListenAndServeTLS("tcp", listenAddr, certFile, keyFile, m) +} + +func runHTTPSWithTLSConfig(listenAddr string, tlsConfig *tls.Config, m http.Handler) error { + return graceful.HTTPListenAndServeTLSConfig("tcp", listenAddr, tlsConfig, m) +} + +// NoHTTPRedirector tells our cleanup routine that we will not be using a fallback http redirector +func NoHTTPRedirector() { + graceful.InformCleanup() +} + +// NoMainListener tells our cleanup routine that we will not be using a possibly provided listener +// for our main HTTP/HTTPS service +func NoMainListener() { + graceful.InformCleanup() } diff --git a/cmd/web_windows.go b/cmd/web_windows.go index 0fc6cbea0d..cdd2cc513b 100644 --- a/cmd/web_windows.go +++ b/cmd/web_windows.go @@ -7,6 +7,7 @@ package cmd import ( + "crypto/tls" "net/http" ) @@ -17,3 +18,20 @@ func runHTTP(listenAddr string, m http.Handler) error { func runHTTPS(listenAddr, certFile, keyFile string, m http.Handler) error { return http.ListenAndServeTLS(listenAddr, certFile, keyFile, m) } + +func runHTTPSWithTLSConfig(listenAddr string, tlsConfig *tls.Config, m http.Handler) error { + server := &http.Server{ + Addr: listenAddr, + Handler: m, + TLSConfig: tlsConfig, + } + return server.ListenAndServeTLS("", "") +} + +// NoHTTPRedirector is a no-op on Windows +func NoHTTPRedirector() { +} + +// NoMainListener is a no-op on Windows +func NoMainListener() { +} diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index 490d6760c8..9c06357295 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -27,13 +27,13 @@ import ( "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/routes" - "github.com/go-xorm/xorm" context2 "github.com/gorilla/context" "github.com/unknwon/com" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/testfixtures.v2" + "xorm.io/xorm" ) var codeFilePath = "contrib/pr/checkout.go" diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service index d88df4a037..b5aa6ffcb5 100644 --- a/contrib/systemd/gitea.service +++ b/contrib/systemd/gitea.service @@ -2,11 +2,41 @@ Description=Gitea (Git with a cup of tea) After=syslog.target After=network.target +### +# Don't forget to add the database service requirements +### +# #Requires=mysql.service #Requires=mariadb.service #Requires=postgresql.service #Requires=memcached.service #Requires=redis.service +# +### +# If using socket activation for main http/s +### +# +#After=gitea.main.socket +#Requires=gitea.main.socket +# +### +# (You can also provide gitea an http fallback and/or ssh socket too) +# +# An example of /etc/systemd/system/gitea.main.socket +### +## +## [Unit] +## Description=Gitea Web Socket +## PartOf=gitea.service +## +## [Socket] +## ListenStream= +## NoDelay=true +## +## [Install] +## WantedBy=sockets.target +## +### [Service] # Modify these two values and uncomment them if you have @@ -20,14 +50,18 @@ Type=simple User=git Group=git WorkingDirectory=/var/lib/gitea/ +# If using unix socket: Tells Systemd to create /run/gitea folder to home gitea.sock +# Manual cration would vanish after reboot. +#RuntimeDirectory=gitea ExecStart=/usr/local/bin/gitea web -c /etc/gitea/app.ini Restart=always Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea -# If you want to bind Gitea to a port below 1024 uncomment -# the two values below +# If you want to bind Gitea to a port below 1024, uncomment +# the two values below, or use socket activation to pass Gitea its ports as above ### #CapabilityBoundingSet=CAP_NET_BIND_SERVICE #AmbientCapabilities=CAP_NET_BIND_SERVICE +### [Install] WantedBy=multi-user.target diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 9bfddc97e8..f0204bb06e 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -74,6 +74,37 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP] ; List of reasons why a Pull Request or Issue can be locked LOCK_REASONS=Too heated,Off-topic,Resolved,Spam +[repository.signing] +; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey +; run in the context of the RUN_USER +; Switch to none to stop signing completely +SIGNING_KEY = default +; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer. +; These should match a publicized name and email address for the key. (When SIGNING_KEY is default these are set to +; the results of git config --get user.name and git config --get user.email respectively and can only be overrided +; by setting the SIGNING_KEY ID to the correct ID.) +SIGNING_NAME = +SIGNING_EMAIL = +; Determines when gitea should sign the initial commit when creating a repository +; Either: +; - never +; - pubkey: only sign if the user has a pubkey +; - twofa: only sign if the user has logged in with twofa +; - always +; options other than none and always can be combined as comma separated list +INITIAL_COMMIT = always +; Determines when to sign for CRUD actions +; - as above +; - parentsigned: requires that the parent commit is signed. +CRUD_ACTIONS = pubkey, twofa, parentsigned +; Determines when to sign Wiki commits +; - as above +WIKI = never +; Determines when to sign on merges +; - basesigned: require that the parent of commit on the base repo is signed. +; - commitssigned: require that all the commits in the head branch are signed. +MERGES = pubkey, twofa, basesigned, commitssigned + [cors] ; More information about CORS can be found here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers ; enable cors headers (disabled by default) @@ -141,8 +172,9 @@ KEYWORDS = go,git,self-hosted,gitea [markdown] ; Enable hard line break extension ENABLE_HARD_LINE_BREAK = false -; List of custom URL-Schemes that are allowed as links when rendering Markdown -; for example git,magnet +; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown +; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes) +; URLs starting with http and https are always displayed, whatever is put in this entry. CUSTOM_URL_SCHEMES = ; List of file extensions that should be rendered/edited as Markdown ; Separate the extensions with a comma. To render files without any extension as markdown, just put a comma @@ -153,6 +185,8 @@ FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd PROTOCOL = http DOMAIN = localhost ROOT_URL = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ +; when STATIC_URL_PREFIX is empty it will follow APP_URL +STATIC_URL_PREFIX = ; The address to listen on. Either a IPv4/IPv6 address or the path to a unix socket. HTTP_ADDR = 0.0.0.0 HTTP_PORT = 3000 @@ -243,6 +277,14 @@ LFS_CONTENT_PATH = data/lfs LFS_JWT_SECRET = ; LFS authentication validity period (in time.Duration), pushes taking longer than this may fail. LFS_HTTP_AUTH_EXPIRY = 20m +; Allow graceful restarts using SIGHUP to fork +ALLOW_GRACEFUL_RESTARTS = true +; After a restart the parent will finish ongoing requests before +; shutting down. Force shutdown if this process takes longer than this delay. +; set to a negative value to disable +GRACEFUL_HAMMER_TIME = 60s +; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time, default is 6h +STATIC_CACHE_TIME = 6h ; Define allowed algorithms and their minimum key length (use -1 to disable a type) [ssh.minimum_key_sizes] @@ -277,10 +319,12 @@ LOG_SQL = true DB_RETRIES = 10 ; Backoff time per DB retry (time.Duration) DB_RETRY_BACKOFF = 3s -; Max idle database connections on connnection pool, default is 0 -MAX_IDLE_CONNS = 0 -; Database connection max life time, default is 3s +; Max idle database connections on connnection pool, default is 2 +MAX_IDLE_CONNS = 2 +; Database connection max life time, default is 0 or 3s mysql (See #6804 & #7071 for reasoning) CONN_MAX_LIFETIME = 3s +; Database maximum number of open connections, default is 0 meaning no maximum +MAX_OPEN_CONNS = 0 [indexer] ; Issue indexer type, currently support: bleve or db, default is bleve @@ -296,6 +340,9 @@ ISSUE_INDEXER_QUEUE_DIR = indexers/issues.queue ISSUE_INDEXER_QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0" ; Batch queue number, default is 20 ISSUE_INDEXER_QUEUE_BATCH_NUMBER = 20 +; Timeout the indexer if it takes longer than this to start. +; Set to zero to disable timeout. +STARTUP_TIMEOUT=30s ; repo indexer by default disabled, since it uses a lot of disk space REPO_INDEXER_ENABLED = false @@ -332,6 +379,10 @@ MIN_PASSWORD_LENGTH = 6 IMPORT_LOCAL_PATHS = false ; Set to true to prevent all users (including admin) from creating custom git hooks DISABLE_GIT_HOOKS = false +;Comma separated list of character classes required to pass minimum complexity. +;If left empty or no valid values are specified, the default values ("lower,upper,digit,spec") will be used. +;Use "off" to disable checking. +PASSWORD_COMPLEXITY = lower,upper,digit,spec ; Password Hash algorithm, either "pbkdf2", "argon2", "scrypt" or "bcrypt" PASSWORD_HASH_ALGO = pbkdf2 ; Set false to allow JavaScript to read CSRF cookie @@ -389,6 +440,10 @@ ALLOW_ONLY_EXTERNAL_REGISTRATION = false REQUIRE_SIGNIN_VIEW = false ; Mail notification ENABLE_NOTIFY_MAIL = false +; This setting enables gitea to be signed in with HTTP BASIC Authentication using the user's password +; If you set this to false you will not be able to access the tokens endpoints on the API with your password +; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token +ENABLE_BASIC_AUTHENTICATION = true ; More detail: https://github.com/gogits/gogs/issues/165 ENABLE_REVERSE_PROXY_AUTHENTICATION = false ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false @@ -415,7 +470,7 @@ DEFAULT_ALLOW_CREATE_ORGANIZATION = true ; Public is for everyone DEFAULT_ORG_VISIBILITY = public ; Default value for DefaultOrgMemberVisible -; True will make the membership of the users visible when added to the organisation +; True will make the membership of the users visible when added to the organisation DEFAULT_ORG_MEMBER_VISIBLE = false ; Default value for EnableDependencies ; Repositories will use dependencies by default depending on this setting @@ -690,6 +745,11 @@ SCHEDULE = @every 24h ; or only create new users if UPDATE_EXISTING is set to false UPDATE_EXISTING = true +; Update migrated repositories' issues and comments' posterid, it will always attempt synchronization when the instance starts. +[cron.update_migration_post_id] +; Interval as a duration between each synchronization. (default every 24h) +SCHEDULE = @every 24h + [git] ; The path of git executable. If empty, Gitea searches through the PATH environment. PATH = @@ -808,3 +868,12 @@ IS_INPUT_FILE = false ENABLED = false ; If you want to add authorization, specify a token here TOKEN = + +[task] +; Task queue type, could be `channel` or `redis`. +QUEUE_TYPE = channel +; Task queue length, available only when `QUEUE_TYPE` is `channel`. +QUEUE_LENGTH = 1000 +; Task queue connection string, available only when `QUEUE_TYPE` is `redis`. +; If there is a password of redis, use `addrs=127.0.0.1:6379 password=123 db=0`. +QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0" diff --git a/docker/root/etc/s6/openssh/setup b/docker/root/etc/s6/openssh/setup index 10d195b74f..2a5eb9b09f 100755 --- a/docker/root/etc/s6/openssh/setup +++ b/docker/root/etc/s6/openssh/setup @@ -26,6 +26,7 @@ fi if [ -d /etc/ssh ]; then SSH_PORT=${SSH_PORT:-"22"} \ + SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \ envsubst < /etc/templates/sshd_config > /etc/ssh/sshd_config chmod 0644 /etc/ssh/sshd_config diff --git a/docker/root/etc/templates/sshd_config b/docker/root/etc/templates/sshd_config index bf0b936d7c..20e0b36012 100644 --- a/docker/root/etc/templates/sshd_config +++ b/docker/root/etc/templates/sshd_config @@ -1,4 +1,4 @@ -Port ${SSH_PORT} +Port ${SSH_LISTEN_PORT} Protocol 2 AddressFamily any @@ -30,4 +30,4 @@ AllowUsers ${USER} Banner none Subsystem sftp /usr/lib/ssh/sftp-server -AcceptEnv GIT_PROTOCOL \ No newline at end of file +AcceptEnv GIT_PROTOCOL diff --git a/docs/content/doc/advanced/api-usage.en-us.md b/docs/content/doc/advanced/api-usage.en-us.md index 8e0b43ec24..624d639545 100644 --- a/docs/content/doc/advanced/api-usage.en-us.md +++ b/docs/content/doc/advanced/api-usage.en-us.md @@ -68,6 +68,14 @@ curl -X POST "http://localhost:4000/api/v1/repos/test1/test1/issues" \ As mentioned above, the token used is the same one you would use in the `token=` string in a GET request. +## API Guide: + +API Reference guide is auto-generated by swagger and available on: + `https://gitea.your.host/api/swagger` + or on + [gitea demo instance](https://try.gitea.io/api/swagger) + + ## Listing your issued tokens via the API As mentioned in diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 198cff6f04..c2744b2958 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -76,6 +76,25 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked +### Repository - Signing (`repository.signing`) + +- `SIGNING_KEY`: **default**: \[none, KEYID, default \]: Key to sign with. +- `SIGNING_NAME` & `SIGNING_EMAIL`: if a KEYID is provided as the `SIGNING_KEY`, use these as the Name and Email address of the signer. These should match publicized name and email address for the key. +- `INITIAL_COMMIT`: **always**: \[never, pubkey, twofa, always\]: Sign initial commit. + - `never`: Never sign + - `pubkey`: Only sign if the user has a public key + - `twofa`: Only sign if the user is logged in with twofa + - `always`: Always sign + - Options other than `never` and `always` can be combined as a comma separated list. +- `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki. +- `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions. + - Options as above, with the addition of: + - `parentsigned`: Only sign if the parent commit is signed. +- `MERGES`: **pubkey, twofa, basesigned, commitssigned**: \[never, pubkey, twofa, basesigned, commitssigned, always\]: Sign merges. + - `basesigned`: Only sign if the parent commit in the base repo is signed. + - `headsigned`: Only sign if the head commit in the head branch is signed. + - `commitssigned`: Only sign if all the commits in the head branch to the merge point are signed. + ## CORS (`cors`) - `ENABLED`: **false**: enable cors headers (disabled by default) @@ -108,6 +127,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. ## Markdown (`markdown`) - `ENABLE_HARD_LINE_BREAK`: **false**: Enable Markdown's hard line break extension. +- `CUSTOM_URL_SCHEMES`: Use a comma separated list (ftp,git,svn) to indicate additional + URL hyperlinks to be rendered in Markdown. URLs beginning in http and https are + always displayed ## Server (`server`) @@ -116,6 +138,13 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `ROOT_URL`: **%(PROTOCOL)s://%(DOMAIN)s:%(HTTP\_PORT)s/**: Overwrite the automatically generated public URL. This is useful if the internal and the external URL don't match (e.g. in Docker). +- `STATIC_URL_PREFIX`: **\**: + Overwrite this option to request static resources from a different URL. + This includes CSS files, images, JS files and web fonts. + Avatar images are dynamic resources and still served by gitea. + The option can be just a different path, as in `/static`, or another domain, as in `https://cdn.example.com`. + Requests are then made as `%(ROOT_URL)s/static/css/index.css` and `https://cdn.example.com/css/index.css` respective. + The static files are located in the `public/` directory of the gitea source repository. - `HTTP_ADDR`: **0.0.0.0**: HTTP listen address. - If `PROTOCOL` is set to `fcgi`, Gitea will listen for FastCGI requests on TCP socket defined by `HTTP_ADDR` and `HTTP_PORT` configuration settings. @@ -140,6 +169,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `CERT_FILE`: **custom/https/cert.pem**: Cert file path used for HTTPS. - `KEY_FILE`: **custom/https/key.pem**: Key file path used for HTTPS. - `STATIC_ROOT_PATH`: **./**: Upper level of template and static files path. +- `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. - `ENABLE_GZIP`: **false**: Enables application-level GZIP support. - `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore\]. - `LFS_START_SERVER`: **false**: Enables git-lfs support. @@ -153,6 +183,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `LETSENCRYPT_ACCEPTTOS`: **false**: This is an explicit check that you accept the terms of service for Let's Encrypt. - `LETSENCRYPT_DIRECTORY`: **https**: Directory that Letsencrypt will use to cache information such as certs and private keys. - `LETSENCRYPT_EMAIL`: **email@example.com**: Email used by Letsencrypt to notify about problems with issued certificates. (No default) +- `ALLOW_GRACEFUL_RESTARTS`: **true**: Perform a graceful restart on SIGHUP +- `GRACEFUL_HAMMER_TIME`: **60s**: After a restart the parent process will stop accepting new connections and will allow requests to finish before stopping. Shutdown will be forced if it takes longer than this time. ## Database (`database`) @@ -167,8 +199,12 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `LOG_SQL`: **true**: Log the executed SQL. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. - `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occured. -- `MAX_IDLE_CONNS` **0**: Max idle database connections on connnection pool, default is 0 -- `CONN_MAX_LIFETIME` **3s**: Database connection max lifetime +- `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit. +- `MAX_IDLE_CONNS` **2**: Max idle database connections on connnection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`. +- `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071). + +Please see #8540 & #8273 for further discussion of the appropriate values for `MAX_OPEN_CONNS`, `MAX_IDLE_CONNS` & `CONN_MAX_LIFETIME` and their +relation to port exhaustion. ## Indexer (`indexer`) @@ -185,6 +221,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `REPO_INDEXER_EXCLUDE`: **empty**: A comma separated list of glob patterns (see https://github.com/gobwas/glob) to **exclude** from the index. Files that match this list will not be indexed, even if they match in `REPO_INDEXER_INCLUDE`. - `UPDATE_BUFFER_LEN`: **20**: Buffer length of index request. - `MAX_FILE_SIZE`: **1048576**: Maximum size in bytes of files to be indexed. +- `STARTUP_TIMEOUT`: **30s**: If the indexer takes longer than this timeout to start - fail. (This timeout will be added to the hammer time above for child processes - as bleve will not start until the previous parent is shutdown.) Set to zero to never timeout. ## Admin (`admin`) - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled @@ -208,6 +245,12 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `INTERNAL_TOKEN_URI`: ****: Instead of defining internal token in the configuration, this configuration option can be used to give Gitea a path to a file that contains the internal token (example value: `file:/etc/gitea/internal_token`) - `PASSWORD_HASH_ALGO`: **pbkdf2**: The hash algorithm to use \[pbkdf2, argon2, scrypt, bcrypt\]. - `CSRF_COOKIE_HTTP_ONLY`: **true**: Set false to allow JavaScript to read CSRF cookie. +- `PASSWORD_COMPLEXITY`: **lower,upper,digit,spec**: Comma separated list of character classes required to pass minimum complexity. If left empty or no valid values are specified, the default values will be used. Possible values are: + - lower - use one or more lower latin characters + - upper - use one or more upper latin characters + - digit - use one or more digits + - spec - use one or more special characters as ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~`` + - off - do not check password complexity ## OpenID (`openid`) @@ -233,6 +276,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `REQUIRE_SIGNIN_VIEW`: **false**: Enable this to force users to log in to view any page. - `ENABLE_NOTIFY_MAIL`: **false**: Enable this to send e-mail to watchers of a repository when something happens, like creating issues. Requires `Mailer` to be enabled. +- `ENABLE_BASIC_AUTHENTICATION`: **true**: Disable this to disallow authenticaton using HTTP + BASIC and the user's password. Please note if you disable this you will not be able to access the + tokens API endpoints using a password. Further, this only disables BASIC authentication using the + password - not tokens or OAuth Basic. - `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication. - `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration for reverse authentication. @@ -419,6 +466,10 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false` - `RUN_AT_START`: **true**: Run repository statistics check at start time. - `SCHEDULE`: **@every 24h**: Cron syntax for scheduling repository statistics check. +### Cron - Update Migration Poster ID (`cron.update_migration_post_id`) + +- `SCHEDULE`: **@every 24h** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts. + ## Git (`git`) - `PATH`: **""**: The path of git executable. If empty, Gitea searches through the PATH environment. @@ -514,9 +565,16 @@ Two special environment variables are passed to the render command: - `GITEA_PREFIX_RAW`, which contains the current URL prefix in the `raw` path tree. To be used as prefix for image paths. ## Time (`time`) + - `FORMAT`: Time format to diplay on UI. i.e. RFC1123 or 2006-01-02 15:04:05 - `DEFAULT_UI_LOCATION`: Default location of time on the UI, so that we can display correct user's time on UI. i.e. Shanghai/Asia +## Task (`task`) + +- `QUEUE_TYPE`: **channel**: Task queue type, could be `channel` or `redis`. +- `QUEUE_LENGTH`: **1000**: Task queue length, available only when `QUEUE_TYPE` is `channel`. +- `QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: Task queue connection string, available only when `QUEUE_TYPE` is `redis`. If there redis needs a password, use `addrs=127.0.0.1:6379 password=123 db=0`. + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: **false**: Show Gitea branding in the footer. diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index 541d66f4e9..a0e33c6370 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -65,6 +65,7 @@ menu: - `CERT_FILE`: 启用HTTPS的证书文件。 - `KEY_FILE`: 启用HTTPS的密钥文件。 - `STATIC_ROOT_PATH`: 存放模板和静态文件的根目录,默认是 Gitea 的根目录。 +- `STATIC_CACHE_TIME`: **6h**: 静态资源文件,包括 `custom/`, `public/` 和所有上传的头像的浏览器缓存时间。 - `ENABLE_GZIP`: 启用应用级别的 GZIP 压缩。 - `LANDING_PAGE`: 未登录用户的默认页面,可选 `home` 或 `explore`。 - `LFS_START_SERVER`: 是否启用 git-lfs 支持. 可以为 `true` 或 `false`, 默认是 `false`。 @@ -196,7 +197,11 @@ menu: ### Cron - Repository Statistics Check (`cron.check_repo_stats`) - `RUN_AT_START`: 是否启动时自动运行仓库统计。 -- `SCHEDULE`: 藏亏统计时的Cron 语法,比如:`@every 24h`. +- `SCHEDULE`: 仓库统计时的Cron 语法,比如:`@every 24h`. + +### Cron - Update Migration Poster ID (`cron.update_migration_post_id`) + +- `SCHEDULE`: **@every 24h** : 每次同步的间隔时间。此任务总是在启动时自动进行。 ## Git (`git`) @@ -241,9 +246,16 @@ IS_INPUT_FILE = false - IS_INPUT_FILE: 输入方式是最后一个参数为文件路径还是从标准输入读取。 ## Time (`time`) + - `FORMAT`: 显示在界面上的时间格式。比如: RFC1123 或者 2006-01-02 15:04:05 - `DEFAULT_UI_LOCATION`: 默认显示在界面上的时区,默认为本地时区。比如: Asia/Shanghai +## Task (`task`) + +- `QUEUE_TYPE`: **channel**: 任务队列类型,可以为 `channel` 或 `redis`。 +- `QUEUE_LENGTH`: **1000**: 任务队列长度,当 `QUEUE_TYPE` 为 `channel` 时有效。 +- `QUEUE_CONN_STR`: **addrs=127.0.0.1:6379 db=0**: 任务队列连接字符串,当 `QUEUE_TYPE` 为 `redis` 时有效。如果redis有密码,则可以 `addrs=127.0.0.1:6379 password=123 db=0`。 + ## Other (`other`) - `SHOW_FOOTER_BRANDING`: 为真则在页面底部显示Gitea的字样。 diff --git a/docs/content/doc/advanced/external-renderers.en-us.md b/docs/content/doc/advanced/external-renderers.en-us.md index e3a122448d..a14f344e63 100644 --- a/docs/content/doc/advanced/external-renderers.en-us.md +++ b/docs/content/doc/advanced/external-renderers.en-us.md @@ -51,7 +51,7 @@ add one `[markup.XXXXX]` section per external renderer on your custom `app.ini`: [markup.asciidoc] ENABLED = true FILE_EXTENSIONS = .adoc,.asciidoc -RENDER_COMMAND = "asciidoctor --out-file=- -" +RENDER_COMMAND = "asciidoctor -e -a leveloffset=-1 --out-file=- -" ; Input is not a standard input but a file IS_INPUT_FILE = false diff --git a/docs/content/doc/advanced/signing.en-us.md b/docs/content/doc/advanced/signing.en-us.md new file mode 100644 index 0000000000..b6c99e269e --- /dev/null +++ b/docs/content/doc/advanced/signing.en-us.md @@ -0,0 +1,162 @@ +--- +date: "2019-08-17T10:20:00+01:00" +title: "GPG Commit Signatures" +slug: "signing" +weight: 20 +toc: false +draft: false +menu: + sidebar: + parent: "advanced" + name: "GPG Commit Signatures" + weight: 20 + identifier: "signing" +--- + +# GPG Commit Signatures + +Gitea will verify GPG commit signatures in the provided tree by +checking if the commits are signed by a key within the gitea database, +or if the commit matches the default key for git. + +Keys are not checked to determine if they have expired or revoked. +Keys are also not checked with keyservers. + +A commit will be marked with a grey unlocked icon if no key can be +found to verify it. If a commit is marked with a red unlocked icon, +it is reported to be signed with a key with an id. + +Please note: The signer of a commit does not have to be an author or +committer of a commit. + +This functionality requires git >= 1.7.9 but for full functionality +this requires git >= 2.0.0. + +## Automatic Signing + +There are a number of places where Gitea will generate commits itself: + +* Repository Initialisation +* Wiki Changes +* CRUD actions using the editor or the API +* Merges from Pull Requests + +Depending on configuration and server trust you may want Gitea to +sign these commits. + +## General Configuration + +Gitea's configuration for signing can be found with the +`[repository.signing]` section of `app.ini`: + +```ini +... +[repository.signing] +SIGNING_KEY = default +SIGNING_NAME = +SIGNING_EMAIL = +INITIAL_COMMIT = always +CRUD_ACTIONS = pubkey, twofa, parentsigned +WIKI = never +MERGES = pubkey, twofa, basesigned, commitssigned + +... +``` + +### `SIGNING_KEY` + +The first option to discuss is the `SIGNING_KEY`. There are three main +options: + +* `none` - this prevents Gitea from signing any commits +* `default` - Gitea will default to the key configured within +`git config` +* `KEYID` - Gitea will sign commits with the gpg key with the ID +`KEYID`. In this case you should provide a `SIGNING_NAME` and +`SIGNING_EMAIL` to be displayed for this key. + +The `default` option will interrogate `git config` for +`commit.gpgsign` option - if this is set, then it will use the results +of the `user.signingkey`, `user.name` and `user.email` as appropriate. + +Please note: by adjusting git's `config` file within Gitea's +repositories, `SIGNING_KEY=default` could be used to provide different +signing keys on a per-repository basis. However, this is cleary not an +ideal UI and therefore subject to change. + +### `INITIAL_COMMIT` + +This option determines whether Gitea should sign the initial commit +when creating a repository. The possible values are: + +* `never`: Never sign +* `pubkey`: Only sign if the user has a public key +* `twofa`: Only sign if the user logs in with two factor authentication +* `always`: Always sign + +Options other than `never` and `always` can be combined as a comma +separated list. + +### `WIKI` + +This options determines if Gitea should sign commits to the Wiki. +The possible values are: + +* `never`: Never sign +* `pubkey`: Only sign if the user has a public key +* `twofa`: Only sign if the user logs in with two factor authentication +* `parentsigned`: Only sign if the parent commit is signed. +* `always`: Always sign + +Options other than `never` and `always` can be combined as a comma +separated list. + +### `CRUD_ACTIONS` + +This option determines if Gitea should sign commits from the web +editor or API CRUD actions. The possible values are: + +* `never`: Never sign +* `pubkey`: Only sign if the user has a public key +* `twofa`: Only sign if the user logs in with two factor authentication +* `parentsigned`: Only sign if the parent commit is signed. +* `always`: Always sign + +Options other than `never` and `always` can be combined as a comma +separated list. + +### `MERGES` + +This option determines if Gitea should sign merge commits from PRs. +The possible options are: + +* `never`: Never sign +* `pubkey`: Only sign if the user has a public key +* `twofa`: Only sign if the user logs in with two factor authentication +* `basesigned`: Only sign if the parent commit in the base repo is signed. +* `headsigned`: Only sign if the head commit in the head branch is signed. +* `commitssigned`: Only sign if all the commits in the head branch to the merge point are signed. +* `always`: Always sign + +Options other than `never` and `always` can be combined as a comma +separated list. + +## Installing and generating a GPG key for Gitea + +It is up to a server administrator to determine how best to install +a signing key. Gitea generates all its commits using the server `git` +command at present - and therefore the server `gpg` will be used for +signing (if configured.) Administrators should review best-practices +for gpg - in particular it is probably advisable to only install a +signing secret subkey without the master signing and certifying secret +key. + +## Obtaining the Public Key of the Signing Key + +The public key used to sign Gitea's commits can be obtained from the API at: + +```/api/v1/signing-key.gpg``` + +In cases where there is a repository specific key this can be obtained from: + +```/api/v1/repos/:username/:reponame/signing-key.gpg``` diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 6bbbfba3b4..1d36d855c2 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -105,6 +105,7 @@ _Symbols used in table:_ | Revert specific commits or a merge request | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | | Pull/Merge requests templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | | Cherry-picking changes | [✘](https://github.com/go-gitea/gitea/issues/5158) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Download Patch | ✓ | ✘ | ✓ | ✓ | ✓ | [/](https://jira.atlassian.com/plugins/servlet/mobile#issue/BCLOUD-8323) | ✘ | #### 3rd-party integrations diff --git a/docs/content/doc/features/webhooks.en-us.md b/docs/content/doc/features/webhooks.en-us.md index 628afb7356..1a0a180e7a 100644 --- a/docs/content/doc/features/webhooks.en-us.md +++ b/docs/content/doc/features/webhooks.en-us.md @@ -17,7 +17,15 @@ menu: Gitea supports web hooks for repository events. This can be found in the settings page `/:username/:reponame/settings/hooks`. All event pushes are POST requests. -The two methods currently supported are Gitea and Slack. +The methods currently supported are: + +- Gitea +- Gogs +- Slack +- Discord +- Dingtalk +- Telegram +- Microsoft Teams ### Event information @@ -104,3 +112,75 @@ X-Gitea-Event: push } } ``` + +### Example + +This is an example of how to use webhooks to run a php script upon push requests to the repository. +In your repository Settings, under Webhooks, Setup a Gitea webhook as follows: + +- Target URL: http://mydomain.com/webhook.php +- HTTP Method: POST +- POST Content Type: application/json +- Secret: 123 +- Trigger On: Push Events +- Active: Checked + +Now on your server create the php file webhook.php + +``` +}}) as well! +**NOTE:** Many of the following directories can be configured using [Environment Variables]({{< relref "doc/advanced/specific-variables.en-us.md" >}}) as well! Of note, configuring `GITEA_WORK_DIR` will tell Gitea where to base its working directory, as well as ease installation. ### Prepare environment @@ -80,7 +80,7 @@ chmod 770 /etc/gitea **NOTE:** `/etc/gitea` is temporary set with write rights for user `git` so that Web installer could write configuration file. After installation is done, it is recommended to set rights to read-only using: ``` chmod 750 /etc/gitea -chmod 644 /etc/gitea/app.ini +chmod 640 /etc/gitea/app.ini ``` If you don't want the web installer to be able to write the config file at all, it is also possible to make the config file read-only for the gitea user (owner/group `root:root`, mode `0660`), and set `INSTALL_LOCK = true`. In that case all database configuration details must be set beforehand in the config file, as well as the `SECRET_KEY` and `INTERNAL_TOKEN` values. See the [command line documentation]({{< relref "doc/usage/command-line.en-us.md" >}}) for information on using `gitea generate secret INTERNAL_TOKEN`. @@ -113,16 +113,16 @@ GITEA_WORK_DIR=/var/lib/gitea/ /usr/local/bin/gitea web -c /etc/gitea/app.ini ## Updating to a new version -You can update to a new version of Gitea by stopping Gitea, replacing the binary at `/usr/local/bin/gitea` and restarting the instance. -The binary file name should not be changed during the update to avoid problems -in existing repositories. +You can update to a new version of Gitea by stopping Gitea, replacing the binary at `/usr/local/bin/gitea` and restarting the instance. +The binary file name should not be changed during the update to avoid problems +in existing repositories. It is recommended you do a [backup]({{< relref "doc/usage/backup-and-restore.en-us.md" >}}) before updating your installation. -If you have carried out the installation steps as described above, the binary should -have the generic name `gitea`. Do not change this, i.e. to include the version number. +If you have carried out the installation steps as described above, the binary should +have the generic name `gitea`. Do not change this, i.e. to include the version number. -See below for troubleshooting instructions to repair broken repositories after +See below for troubleshooting instructions to repair broken repositories after an update of your Gitea version. ## Troubleshooting @@ -145,7 +145,7 @@ is already running. ### Running Gitea on Raspbian -As of v1.8, there is a problem with the arm7 version of Gitea and it doesn't run on Raspberry Pi and similar devices. +As of v1.8, there is a problem with the arm7 version of Gitea and it doesn't run on Raspberry Pi and similar devices. It is therefore recommended to switch to the arm6 version which has been tested and shown to work on Raspberry Pi and similar devices. @@ -154,18 +154,18 @@ please remove after fixing the arm7 bug ---> ### Git error after updating to a new version of Gitea -If the binary file name has been changed during the update to a new version of Gitea, -git hooks in existing repositories will not work any more. In that case, a git +If the binary file name has been changed during the update to a new version of Gitea, +git hooks in existing repositories will not work any more. In that case, a git error will be displayed when pushing to the repository. ``` remote: ./hooks/pre-receive.d/gitea: line 2: [...]: No such file or directory ``` -The `[...]` part of the error message will contain the path to your previous Gitea +The `[...]` part of the error message will contain the path to your previous Gitea binary. -To solve this, go to the admin options and run the task `Resynchronize pre-receive, +To solve this, go to the admin options and run the task `Resynchronize pre-receive, update and post-receive hooks of all repositories` to update all hooks to contain the new binary path. Please note that this overwrite all git hooks including ones with customizations made. diff --git a/docs/content/doc/installation/from-source.en-us.md b/docs/content/doc/installation/from-source.en-us.md index 9455f93d80..25ed5b790a 100644 --- a/docs/content/doc/installation/from-source.en-us.md +++ b/docs/content/doc/installation/from-source.en-us.md @@ -118,12 +118,12 @@ launched manually from command line, it can be killed by pressing `Ctrl + C`. ./gitea web ``` -## Changing the default CustomPath, CustomConf and AppWorkDir +## Changing the default CustomPath, CustomConf and AppWorkPath Gitea will search for a number of things from the `CustomPath`. By default this is the `custom/` directory in the current working directory when running Gitea. It will also look for its configuration file `CustomConf` in `$CustomPath/conf/app.ini`, and will use the -current working directory as the relative base path `AppWorkDir` for a number configurable +current working directory as the relative base path `AppWorkPath` for a number configurable values. These values, although useful when developing, may conflict with downstream users preferences. @@ -134,7 +134,7 @@ using the `LDFLAGS` environment variable for `make`. The appropriate settings ar * To set the `CustomPath` use `LDFLAGS="-X \"code.gitea.io/gitea/modules/setting.CustomPath=custom-path\""` * For `CustomConf` you should use `-X \"code.gitea.io/gitea/modules/setting.CustomConf=conf.ini\"` -* For `AppWorkDir` you should use `-X \"code.gitea.io/gitea/modules/setting.AppWorkDir=working-directory\"` +* For `AppWorkPath` you should use `-X \"code.gitea.io/gitea/modules/setting.AppWorkPath=working-path\"` Add as many of the strings with their preceding `-X` to the `LDFLAGS` variable and run `make build` with the appropriate `TAGS` as above. diff --git a/docs/content/doc/usage/email-setup.md b/docs/content/doc/usage/email-setup.md new file mode 100644 index 0000000000..6fc9de81dd --- /dev/null +++ b/docs/content/doc/usage/email-setup.md @@ -0,0 +1,33 @@ +--- +date: "2019-10-15T10:10:00+05:00" +title: "Usage: Email setup" +slug: "email-setup" +weight: 12 +toc: true +draft: false +menu: + sidebar: + parent: "usage" + name: "Email setup" + weight: 12 + identifier: "email-setup" +--- + +# Email setup + +- To use Gitea's built-in Email support, update the `app.ini` config file [mailer] section: + +```ini +[mailer] +ENABLED = true +HOST = mail.mydomain.com:587 +FROM = gitea@mydomain.com +USER = gitea@mydomain.com +PASSWD = `password` +``` + +- Restart Gitea for the configuration changes to take effect. + +- To send a test email to validate the settings, go to Gitea > Site Administration > Configuration > SMTP Mailer Configuration. + +For the full list of options check the [Config Cheat Sheet]({{< relref "doc/advanced/config-cheat-sheet.en-us.md" >}}) \ No newline at end of file diff --git a/docs/content/doc/usage/fail2ban-setup.md b/docs/content/doc/usage/fail2ban-setup.md index 28c4874da2..922c71f93d 100644 --- a/docs/content/doc/usage/fail2ban-setup.md +++ b/docs/content/doc/usage/fail2ban-setup.md @@ -26,7 +26,7 @@ on a bad authentication: 2018/04/26 18:15:54 [I] Failed authentication attempt for user from xxx.xxx.xxx.xxx ``` -So we set our filter in `/etc/fail2ban/filter.d/gitea.conf`: +Add our filter in `/etc/fail2ban/filter.d/gitea.conf`: ```ini # gitea.conf @@ -35,12 +35,11 @@ failregex = .*Failed authentication attempt for .* from ignoreregex = ``` -And configure it in `/etc/fail2ban/jail.d/jail.local`: +Add our jail in `/etc/fail2ban/jail.d/gitea.conf`: ```ini [gitea] enabled = true -port = http,https filter = gitea logpath = /home/git/gitea/log/gitea.log maxretry = 10 @@ -49,6 +48,23 @@ bantime = 900 action = iptables-allports ``` +If you're using Docker, you'll also need to add an additional jail to handle the **FORWARD** +chain in **iptables**. Configure it in `/etc/fail2ban/jail.d/gitea-docker.conf`: + +```ini +[gitea-docker] +enabled = true +filter = gitea +logpath = /home/git/gitea/log/gitea.log +maxretry = 10 +findtime = 3600 +bantime = 900 +action = iptables-allports[chain="FORWARD"] +``` + +Then simply run `service fail2ban restart` to apply your changes. You can check to see if +fail2ban has accepted your configuration using `service fail2ban status`. + Make sure and read up on fail2ban and configure it to your needs, this bans someone for **15 minutes** (from all ports) when they fail authentication 10 times in an hour. diff --git a/docs/content/doc/usage/git-lfs-support.md b/docs/content/doc/usage/git-lfs-support.md new file mode 100644 index 0000000000..2d5fab3cb3 --- /dev/null +++ b/docs/content/doc/usage/git-lfs-support.md @@ -0,0 +1,26 @@ +--- +date: "2019-10-06T08:00:00+05:00" +title: "Usage: Git LFS setup" +slug: "git-lfs-setup" +weight: 12 +toc: true +draft: false +menu: + sidebar: + parent: "usage" + name: "Git LFS setup" + weight: 12 + identifier: "git-lfs-setup" +--- + +# Git Large File Storage setup + +To use Gitea's built-in LFS support, you must update the `app.ini` file: + +```ini +[server] +; Enables git-lfs support. true or false, default is false. +LFS_START_SERVER = true +; Where your lfs files reside, default is data/lfs. +LFS_CONTENT_PATH = /home/gitea/data/lfs +``` \ No newline at end of file diff --git a/docs/content/doc/usage/https-support.md b/docs/content/doc/usage/https-support.md index 22cbc684aa..e2b5332c05 100644 --- a/docs/content/doc/usage/https-support.md +++ b/docs/content/doc/usage/https-support.md @@ -20,6 +20,8 @@ menu: Before you enable HTTPS, make sure that you have valid SSL/TLS certificates. You could use self-generated certificates for evaluation and testing. Please run `gitea cert --host [HOST]` to generate a self signed certificate. +If you are using Apache or nginx on the server, it's recommended to check the [reverse proxy guide]({{< relref "doc/usage/reverse-proxies.en-us.md" >}}). + To use Gitea's built-in HTTPS support, you must change your `app.ini` file: ```ini diff --git a/docs/content/doc/usage/reverse-proxies.en-us.md b/docs/content/doc/usage/reverse-proxies.en-us.md index 47a5b95572..55c8bb9710 100644 --- a/docs/content/doc/usage/reverse-proxies.en-us.md +++ b/docs/content/doc/usage/reverse-proxies.en-us.md @@ -44,6 +44,74 @@ server { Then set `[server] ROOT_URL = http://git.example.com/git/` in your configuration. +## Using Nginx as a reverse proxy and serve static resources directly +We can tune the performance in splitting requests into categories static and dynamic. + +CSS files, JavaScript files, images and web fonts are static content. +The front page, a repository view or issue list is dynamic content. + +Nginx can serve static resources directly and proxy only the dynamic requests to gitea. +Nginx is optimized for serving static content, while the proxying of large responses might be the opposite of that + (see https://serverfault.com/q/587386). + +Download a snap shot of the gitea source repository to `/path/to/gitea/`. + +We are only interested in the `public/` directory and you can delete the rest. + +Depending on the scale of your user base, you might want to split the traffic to two distinct servers, + or use a cdn for the static files. + +### using a single node and a single domain + +Set `[server] STATIC_URL_PREFIX = /_/static` in your configuration. + +``` +server { + listen 80; + server_name git.example.com; + + location /_/static { + alias /path/to/gitea/public; + } + + location / { + proxy_pass http://localhost:3000; + } +} +``` + +### using two nodes and two domains + +Set `[server] STATIC_URL_PREFIX = http://cdn.example.com/gitea` in your configuration. + +``` +# application server running gitea +server { + listen 80; + server_name git.example.com; + + location / { + proxy_pass http://localhost:3000; + } +} +``` + +``` +# static content delivery server +server { + listen 80; + server_name cdn.example.com; + + location /gitea { + alias /path/to/gitea/public; + } + + location / { + return 404; + } +} +``` + ## Using Apache HTTPD as a reverse proxy If you want Apache HTTPD to serve your Gitea instance, you can add the following to your Apache HTTPD configuration (usually located at `/etc/apache2/httpd.conf` in Ubuntu): diff --git a/go.mod b/go.mod index e1a2b7b404..e1bbd9ac89 100644 --- a/go.mod +++ b/go.mod @@ -29,16 +29,12 @@ require ( github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect github.com/denisenkom/go-mssqldb v0.0.0-20190924004331-208c0a498538 github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/editorconfig/editorconfig-core-go/v2 v2.1.1 github.com/emirpasic/gods v1.12.0 github.com/etcd-io/bbolt v1.3.2 // indirect github.com/ethantkoenig/rupture v0.0.0-20180203182544-0a76f03a811a - github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect - github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 // indirect - github.com/facebookgo/grace v0.0.0-20160926231715-5729e484473f - github.com/facebookgo/httpdown v0.0.0-20160323221027-a3b1354551a2 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect - github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 // indirect github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect github.com/gliderlabs/ssh v0.2.2 github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd // indirect @@ -48,7 +44,6 @@ require ( github.com/go-redis/redis v6.15.2+incompatible github.com/go-sql-driver/mysql v1.4.1 github.com/go-swagger/go-swagger v0.20.1 - github.com/go-xorm/xorm v0.7.9 github.com/gobwas/glob v0.2.3 github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561 github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 @@ -64,7 +59,7 @@ require ( github.com/klauspost/compress v0.0.0-20161025140425-8df558b6cb6f github.com/klauspost/cpuid v0.0.0-20160302075316-09cded8978dc // indirect github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6 // indirect - github.com/lafriks/xormstore v1.3.1 + github.com/lafriks/xormstore v1.3.2 github.com/lib/pq v1.2.0 github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 github.com/lunny/levelqueue v0.0.0-20190217115915-02b525a4418e @@ -112,16 +107,15 @@ require ( golang.org/x/tools v0.0.0-20190910221609-7f5965fd7709 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175 // indirect - gopkg.in/editorconfig/editorconfig-core-go.v1 v1.3.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df - gopkg.in/ini.v1 v1.46.0 + gopkg.in/ini.v1 v1.48.0 gopkg.in/ldap.v3 v3.0.2 gopkg.in/src-d/go-billy.v4 v4.3.2 gopkg.in/src-d/go-git.v4 v4.13.1 - gopkg.in/stretchr/testify.v1 v1.2.2 // indirect gopkg.in/testfixtures.v2 v2.5.0 - mvdan.cc/xurls/v2 v2.0.0 - strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a + mvdan.cc/xurls/v2 v2.1.0 + strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.6 xorm.io/core v0.7.2 + xorm.io/xorm v0.8.0 ) diff --git a/go.sum b/go.sum index c068caa2f7..2eeaa79810 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/chaseadamsio/goorgeous v0.0.0-20170901132237-098da33fde5f h1:REH9VH5ubNR0skLaOxK7TRJeRbE2dDfvaouQo8FsRcA= github.com/chaseadamsio/goorgeous v0.0.0-20170901132237-098da33fde5f/go.mod h1:6QaC0vFoKWYDth94dHFNgRT2YkT5FHdQp/Yx15aAAi0= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/corbym/gocrest v1.0.3 h1:gwEdq6RkTmq+09CTuM29DfKOCtZ7G7bcyxs3IZ6EVdU= github.com/corbym/gocrest v1.0.3/go.mod h1:maVFL5lbdS2PgfOQgGRWDYTeunSWQeiEgoNdTABShCs= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -134,6 +132,8 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/editorconfig/editorconfig-core-go/v2 v2.1.1 h1:mhPg/0hGebcpiiQLqJD2PWWyoHRLEdZ3sXKaEvT1EQU= +github.com/editorconfig/editorconfig-core-go/v2 v2.1.1/go.mod h1:/LuhWJiQ9Gvo1DhVpa4ssm5qeg8rrztdtI7j/iCie2k= github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= @@ -142,20 +142,10 @@ github.com/etcd-io/bbolt v1.3.2 h1:RLRQ0TKLX7DlBRXAJHvbmXL17Q3KNnTBtZ9B6Qo+/Y0= github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/ethantkoenig/rupture v0.0.0-20180203182544-0a76f03a811a h1:M1bRpaZAn4GSsqu3hdK2R8H0AH9O6vqCTCbm2oAFGfE= github.com/ethantkoenig/rupture v0.0.0-20180203182544-0a76f03a811a/go.mod h1:MkKY/CB98aVE4VxO63X5vTQKUgcn+3XP15LMASe3lYs= -github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= -github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= -github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9 h1:wWke/RUCl7VRjQhwPlR/v0glZXNYzBHdNUzf/Am2Nmg= -github.com/facebookgo/freeport v0.0.0-20150612182905-d4adf43b75b9/go.mod h1:uPmAp6Sws4L7+Q/OokbWDAK1ibXYhB3PXFP1kol5hPg= -github.com/facebookgo/grace v0.0.0-20160926231715-5729e484473f h1:0mlfEUWnUDVZnqWEVHGerL5bKYDKMEmT/Qk/W/3nGuo= -github.com/facebookgo/grace v0.0.0-20160926231715-5729e484473f/go.mod h1:KigFdumBXUPSwzLDbeuzyt0elrL7+CP7TKuhrhT4bcU= -github.com/facebookgo/httpdown v0.0.0-20160323221027-a3b1354551a2 h1:3Zvf9wRhl1cOhckN1oRGWPOkIhOketmEcrQ4TeFAoR4= -github.com/facebookgo/httpdown v0.0.0-20160323221027-a3b1354551a2/go.mod h1:TUV/fX3XrTtBQb5+ttSUJzcFgLNpILONFTKmBuk5RSw= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= -github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4 h1:0YtRCqIZs2+Tz49QuH6cJVw/IFqzo39gEqZ0iYLxD2M= -github.com/facebookgo/stats v0.0.0-20151006221625-1b76add642e4/go.mod h1:vsJz7uE339KUCpBXx3JAJzSRH7Uk4iGGyJzR529qDIA= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= @@ -249,11 +239,8 @@ github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y= github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM= -github.com/go-xorm/xorm v0.7.9 h1:LZze6n1UvRmM5gpL9/U9Gucwqo6aWlFVlfcHKH10qA0= -github.com/go-xorm/xorm v0.7.9/go.mod h1:XiVxrMMIhFkwSkh96BW7PACl7UhLtx2iJIHMdmjh5sQ= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561 h1:deE7ritpK04PgtpyVOS2TYcQEld9qLCD5b5EbVNOuLA= github.com/gogits/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:YgYOrVn3Nj9Tq0EvjmFbphRytDj7JNRoWSStJZWDJTQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -337,9 +324,6 @@ github.com/issue9/assert v1.3.2 h1:IaTa37u4m1fUuTH9K9ldO5IONKVDXjLiUO1T9vj0OF0= github.com/issue9/assert v1.3.2/go.mod h1:9Ger+iz8X7r1zMYYwEhh++2wMGWcNN2oVI+zIQXxcio= github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c h1:A/PDn117UYld5mlxe58EpMguqpkeTMw5/FCo0ZPS/Ko= github.com/issue9/identicon v0.0.0-20160320065130-d36b54562f4c/go.mod h1:5mTb/PQNkqmq2x3IxlQZE0aSnTksJg7fg/oWmJ5SKXQ= -github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= -github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= -github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d h1:ig/iUfDDg06RVW8OMby+GrmW6K2nPO3AFHlEIdvJSd4= github.com/jaytaylor/html2text v0.0.0-20160923191438-8fb95d837f7d/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= @@ -384,8 +368,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lafriks/xormstore v1.3.1 h1:KpzRUamSV3zmA85Kzw+PZOU9wgMbYsNzuDzLuBMbxpA= -github.com/lafriks/xormstore v1.3.1/go.mod h1:qALRD4Vto2Ic7/A5eplMpu5V62mugtSqFysRwz8FETs= +github.com/lafriks/xormstore v1.3.2 h1:hqi3F8s/B4rz8GuEZZDuHuOxRjeuOpEI/cC7vcnWwH4= +github.com/lafriks/xormstore v1.3.2/go.mod h1:mVNIwIa25QIr8rfR7YlVjrqN/apswHkVdtLCyVYBzXw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -511,8 +495,6 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b h1:4kg1wyftSKxLtnPAvcRWakIPpokB9w780/KwrNLnfPA= github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v0.0.0-20160918041101-1dba4b3954bc h1:3wIrJvFb3Pf6B/2mDBnN1G5IfUVev4X5apadQlWOczE= @@ -532,6 +514,7 @@ github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1 github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -773,17 +756,18 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/editorconfig/editorconfig-core-go.v1 v1.3.0 h1:oxOEwvhxLMpWpN+0pb2r9TWrM0DCFBHxbuIlS27tmFg= -gopkg.in/editorconfig/editorconfig-core-go.v1 v1.3.0/go.mod h1:s2mQFI9McjArkyCwyEwU//+luQENTnD/Lfb/7Sj3/kQ= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg= gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag= gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.48.0 h1:URjZc+8ugRY5mL5uUeQH/a63JcHwdX9xZaWvmNWD7z8= +gopkg.in/ini.v1 v1.48.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ldap.v3 v3.0.2 h1:R6RBtabK6e1GO0eQKtkyOFbAHO73QesLzI2w2DZ6b9w= gopkg.in/ldap.v3 v3.0.2/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= @@ -794,8 +778,6 @@ gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOA gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= -gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= -gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= gopkg.in/testfixtures.v2 v2.5.0 h1:N08B7l2GzFQenyYbzqthDnKAA+cmb17iAZhhFxr7JHw= gopkg.in/testfixtures.v2 v2.5.0/go.mod h1:vyAq+MYCgNpR29qitQdLZhdbLFf4mR/2MFJRFoQZZ2M= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -812,14 +794,14 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -mvdan.cc/xurls/v2 v2.0.0 h1:r1zSOSNS/kqtpmATyMMMvaZ4/djsesbYz5kr0+qMRWc= -mvdan.cc/xurls/v2 v2.0.0/go.mod h1:2/webFPYOXN9jp/lzuj0zuAVlF+9g4KPFJANH1oJhRU= +mvdan.cc/xurls/v2 v2.1.0 h1:KaMb5GLhlcSX+e+qhbRJODnUUBvlw01jt4yrjFIHAuA= +mvdan.cc/xurls/v2 v2.1.0/go.mod h1:5GrSd9rOnKOpZaji1OZLYL/yeAAtGDlo/cFe+8K5n8E= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a h1:8q33ShxKXRwQ7JVd1ZnhIU3hZhwwn0Le+4fTeAackuM= -strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= +strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs= +strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8= xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= -xorm.io/core v0.7.2-0.20190928055935-90aeac8d08eb h1:msX3zG3BPl8Ti+LDzP33/9K7BzO/WqFXk610K1kYKfo= -xorm.io/core v0.7.2-0.20190928055935-90aeac8d08eb/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw= xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM= +xorm.io/xorm v0.8.0 h1:iALxgJrX8O00f8Jk22GbZwPmxJNgssV5Mv4uc2HL9PM= +xorm.io/xorm v0.8.0/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY= diff --git a/integrations/api_helper_for_declarative_test.go b/integrations/api_helper_for_declarative_test.go index 805a986ae3..cae7691c4b 100644 --- a/integrations/api_helper_for_declarative_test.go +++ b/integrations/api_helper_for_declarative_test.go @@ -231,3 +231,38 @@ func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) ctx.Session.MakeRequest(t, req, 200) } } + +func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) { + return func(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + resp := ctx.Session.MakeRequest(t, req, http.StatusOK) + + var branch api.Branch + DecodeJSON(t, resp, &branch) + if len(callback) > 0 { + callback[0](t, branch) + } + } +} + +func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) { + return func(t *testing.T) { + url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", ctx.Username, ctx.Reponame, treepath, ctx.Token) + req := NewRequestWithJSON(t, "POST", url, &options) + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + resp := ctx.Session.MakeRequest(t, req, http.StatusCreated) + + var contents api.FileResponse + DecodeJSON(t, resp, &contents) + if len(callback) > 0 { + callback[0](t, contents) + } + } +} diff --git a/integrations/api_pull_test.go b/integrations/api_pull_test.go index 8d24cdc188..ed5a55a9db 100644 --- a/integrations/api_pull_test.go +++ b/integrations/api_pull_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + issue_service "code.gitea.io/gitea/services/issue" "github.com/stretchr/testify/assert" ) @@ -40,7 +41,7 @@ func TestAPIMergePullWIP(t *testing.T) { owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{Status: models.PullRequestStatusMergeable}, models.Cond("has_merged = ?", false)).(*models.PullRequest) pr.LoadIssue() - pr.Issue.ChangeTitle(owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title) + issue_service.ChangeTitle(pr.Issue, owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title) // force reload pr.LoadAttributes() diff --git a/integrations/api_repo_file_create_test.go b/integrations/api_repo_file_create_test.go index 42898bf259..4d76ff00ce 100644 --- a/integrations/api_repo_file_create_test.go +++ b/integrations/api_repo_file_create_test.go @@ -91,7 +91,7 @@ func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileRespon }, Verification: &api.PayloadCommitVerification{ Verified: false, - Reason: "unsigned", + Reason: "gpg.error.not_signed_commit", Signature: "", Payload: "", }, diff --git a/integrations/api_repo_file_update_test.go b/integrations/api_repo_file_update_test.go index 366eb5e918..bf695d4344 100644 --- a/integrations/api_repo_file_update_test.go +++ b/integrations/api_repo_file_update_test.go @@ -94,7 +94,7 @@ func getExpectedFileResponseForUpdate(commitID, treePath string) *api.FileRespon }, Verification: &api.PayloadCommitVerification{ Verified: false, - Reason: "unsigned", + Reason: "gpg.error.not_signed_commit", Signature: "", Payload: "", }, diff --git a/integrations/api_team_user_test.go b/integrations/api_team_user_test.go index 70d52c1360..4df4dac016 100644 --- a/integrations/api_team_user_test.go +++ b/integrations/api_team_user_test.go @@ -29,7 +29,6 @@ func TestAPITeamUser(t *testing.T) { var user2 *api.User DecodeJSON(t, resp, &user2) user2.Created = user2.Created.In(time.Local) - user2.LastLogin = user2.LastLogin.In(time.Local) user := models.AssertExistsAndLoadBean(t, &models.User{Name: "user2"}).(*models.User) assert.Equal(t, convert.ToUser(user, true, false), user2) diff --git a/integrations/api_user_heatmap_test.go b/integrations/api_user_heatmap_test.go index 5245bb0a26..2e2636ce94 100644 --- a/integrations/api_user_heatmap_test.go +++ b/integrations/api_user_heatmap_test.go @@ -26,7 +26,7 @@ func TestUserHeatmap(t *testing.T) { var heatmap []*models.UserHeatmapData DecodeJSON(t, resp, &heatmap) var dummyheatmap []*models.UserHeatmapData - dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1540080000, Contributions: 1}) + dummyheatmap = append(dummyheatmap, &models.UserHeatmapData{Timestamp: 1571616000, Contributions: 1}) assert.Equal(t, dummyheatmap, heatmap) } diff --git a/integrations/git_helper_for_declarative_test.go b/integrations/git_helper_for_declarative_test.go index 628611d2d7..294f0bc5fe 100644 --- a/integrations/git_helper_for_declarative_test.go +++ b/integrations/git_helper_for_declarative_test.go @@ -12,7 +12,9 @@ import ( "net/http" "net/url" "os" + "path" "path/filepath" + "strings" "testing" "time" @@ -37,7 +39,12 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) { err = ssh.GenKeyPair(keyFile) assert.NoError(t, err) + err = ioutil.WriteFile(path.Join(tmpDir, "ssh"), []byte("#!/bin/bash\n"+ + "ssh -o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\" -o \"IdentitiesOnly=yes\" -i \""+keyFile+"\" \"$@\""), 0700) + assert.NoError(t, err) + //Setup ssh wrapper + os.Setenv("GIT_SSH", path.Join(tmpDir, "ssh")) os.Setenv("GIT_SSH_COMMAND", "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i \""+keyFile+"\"") os.Setenv("GIT_SSH_VARIANT", "ssh") @@ -54,6 +61,24 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL { return &u2 } +func allowLFSFilters() []string { + // Now here we should explicitly allow lfs filters to run + globalArgs := git.GlobalCommandArgs + filteredLFSGlobalArgs := make([]string, len(git.GlobalCommandArgs)) + j := 0 + for _, arg := range git.GlobalCommandArgs { + if strings.Contains(arg, "lfs") { + j-- + } else { + filteredLFSGlobalArgs[j] = arg + j++ + } + } + filteredLFSGlobalArgs = filteredLFSGlobalArgs[:j] + git.GlobalCommandArgs = filteredLFSGlobalArgs + return globalArgs +} + func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL)) { prepareTestEnv(t, 1) s := http.Server{ @@ -79,7 +104,9 @@ func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL)) { func doGitClone(dstLocalPath string, u *url.URL) func(*testing.T) { return func(t *testing.T) { + oldGlobals := allowLFSFilters() assert.NoError(t, git.Clone(u.String(), dstLocalPath, git.CloneRepoOptions{})) + git.GlobalCommandArgs = oldGlobals assert.True(t, com.IsExist(filepath.Join(dstLocalPath, "README.md"))) } } @@ -140,7 +167,9 @@ func doGitCreateBranch(dstPath, branch string) func(*testing.T) { func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { + oldGlobals := allowLFSFilters() _, err := git.NewCommand(append([]string{"checkout"}, args...)...).RunInDir(dstPath) + git.GlobalCommandArgs = oldGlobals assert.NoError(t, err) } } @@ -154,7 +183,9 @@ func doGitMerge(dstPath string, args ...string) func(*testing.T) { func doGitPull(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { + oldGlobals := allowLFSFilters() _, err := git.NewCommand(append([]string{"pull"}, args...)...).RunInDir(dstPath) + git.GlobalCommandArgs = oldGlobals assert.NoError(t, err) } } diff --git a/integrations/git_test.go b/integrations/git_test.go index 8578fb86d5..dbcc265227 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" @@ -135,6 +136,11 @@ func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) { t.Run("LFS", func(t *testing.T) { PrintCurrentTest(t) + setting.CheckLFSVersion() + if !setting.LFS.StartServer { + t.Skip() + return + } prefix := "lfs-data-file-" _, err := git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) assert.NoError(t, err) @@ -142,6 +148,21 @@ func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS strin assert.NoError(t, err) err = git.AddChanges(dstPath, false, ".gitattributes") assert.NoError(t, err) + oldGlobals := allowLFSFilters() + err = git.CommitChanges(dstPath, git.CommitChangesOptions{ + Committer: &git.Signature{ + Email: "user2@example.com", + Name: "User Two", + When: time.Now(), + }, + Author: &git.Signature{ + Email: "user2@example.com", + Name: "User Two", + When: time.Now(), + }, + Message: fmt.Sprintf("Testing commit @ %v", time.Now()), + }) + git.GlobalCommandArgs = oldGlobals littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix) @@ -185,20 +206,25 @@ func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS s resp := session.MakeRequest(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, littleSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + setting.CheckLFSVersion() + if setting.LFS.StartServer { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, littleSize, resp.Body.Len()) + assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + } if !testing.Short() { req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big)) resp = session.MakeRequest(t, req, http.StatusOK) assert.Equal(t, bigSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, bigSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + if setting.LFS.StartServer { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, bigSize, resp.Body.Len()) + assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + } } }) } @@ -217,18 +243,23 @@ func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) assert.Equal(t, littleSize, resp.Length) - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Length) + setting.CheckLFSVersion() + if setting.LFS.StartServer { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Length) + } if !testing.Short() { req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big)) resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) assert.Equal(t, bigSize, resp.Length) - req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) + if setting.LFS.StartServer { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + } } }) } @@ -274,6 +305,8 @@ func generateCommitWithNewData(size int, repoPath, email, fullName, prefix strin } //Commit + // Now here we should explicitly allow lfs filters to run + oldGlobals := allowLFSFilters() err = git.AddChanges(repoPath, false, filepath.Base(tmpFile.Name())) if err != nil { return "", err @@ -291,6 +324,7 @@ func generateCommitWithNewData(size int, repoPath, email, fullName, prefix strin }, Message: fmt.Sprintf("Testing commit @ %v", time.Now()), }) + git.GlobalCommandArgs = oldGlobals return filepath.Base(tmpFile.Name()), err } diff --git a/integrations/gpg_git_test.go b/integrations/gpg_git_test.go new file mode 100644 index 0000000000..12f0a138c7 --- /dev/null +++ b/integrations/gpg_git_test.go @@ -0,0 +1,252 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" +) + +func TestGPGGit(t *testing.T) { + onGiteaRun(t, testGPGGit) +} + +func testGPGGit(t *testing.T, u *url.URL) { + username := "user2" + baseAPITestContext := NewAPITestContext(t, username, "repo1") + + u.Path = baseAPITestContext.GitPath() + + // OK Set a new GPG home + tmpDir, err := ioutil.TempDir("", "temp-gpg") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + err = os.Chmod(tmpDir, 0700) + assert.NoError(t, err) + + oldGNUPGHome := os.Getenv("GNUPGHOME") + err = os.Setenv("GNUPGHOME", tmpDir) + assert.NoError(t, err) + defer os.Setenv("GNUPGHOME", oldGNUPGHome) + + // Need to create a root key + rootKeyPair, err := createGPGKey(tmpDir, "gitea", "gitea@fake.local") + assert.NoError(t, err) + + rootKeyID := rootKeyPair.PrimaryKey.KeyIdShortString() + + oldKeyID := setting.Repository.Signing.SigningKey + oldName := setting.Repository.Signing.SigningName + oldEmail := setting.Repository.Signing.SigningEmail + defer func() { + setting.Repository.Signing.SigningKey = oldKeyID + setting.Repository.Signing.SigningName = oldName + setting.Repository.Signing.SigningEmail = oldEmail + }() + + setting.Repository.Signing.SigningKey = rootKeyID + setting.Repository.Signing.SigningName = "gitea" + setting.Repository.Signing.SigningEmail = "gitea@fake.local" + user := models.AssertExistsAndLoadBean(t, &models.User{Name: username}).(*models.User) + + t.Run("Unsigned-Initial", func(t *testing.T) { + PrintCurrentTest(t) + setting.Repository.Signing.InitialCommit = []string{"never"} + testCtx := NewAPITestContext(t, username, "initial-unsigned") + t.Run("CreateRepository", doAPICreateRepository(testCtx, false)) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + assert.NotNil(t, branch.Commit) + assert.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + setting.Repository.Signing.CRUDActions = []string{"never"} + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + setting.Repository.Signing.CRUDActions = []string{"never"} + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + setting.Repository.Signing.CRUDActions = []string{"always"} + t.Run("CreateCRUDFile-Always", crudActionCreateFile( + t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) + })) + t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) + })) + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile( + t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) + })) + }) + t.Run("AlwaysSign-Initial", func(t *testing.T) { + PrintCurrentTest(t) + setting.Repository.Signing.InitialCommit = []string{"always"} + testCtx := NewAPITestContext(t, username, "initial-always") + t.Run("CreateRepository", doAPICreateRepository(testCtx, false)) + t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + assert.NotNil(t, branch.Commit) + assert.NotNil(t, branch.Commit.Verification) + assert.True(t, branch.Commit.Verification.Verified) + assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email) + })) + setting.Repository.Signing.CRUDActions = []string{"never"} + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) + })) + setting.Repository.Signing.CRUDActions = []string{"always"} + t.Run("CreateCRUDFile-Always", crudActionCreateFile( + t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) + })) + + }) + t.Run("UnsignedMerging", func(t *testing.T) { + PrintCurrentTest(t) + testCtx := NewAPITestContext(t, username, "initial-unsigned") + var pr api.PullRequest + var err error + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t) + assert.NoError(t, err) + }) + setting.Repository.Signing.Merges = []string{"commitssigned"} + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + assert.NotNil(t, branch.Commit) + assert.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + setting.Repository.Signing.Merges = []string{"basesigned"} + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t) + assert.NoError(t, err) + }) + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + assert.NotNil(t, branch.Commit) + assert.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + setting.Repository.Signing.Merges = []string{"commitssigned"} + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t) + assert.NoError(t, err) + }) + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + assert.NotNil(t, branch.Commit) + assert.NotNil(t, branch.Commit.Verification) + assert.True(t, branch.Commit.Verification.Verified) + })) + + }) +} + +func crudActionCreateFile(t *testing.T, ctx APITestContext, user *models.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) { + return doAPICreateFile(ctx, path, &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: from, + NewBranchName: to, + Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path), + Author: api.Identity{ + Name: user.FullName, + Email: user.Email, + }, + Committer: api.Identity{ + Name: user.FullName, + Email: user.Email, + }, + }, + Content: base64.StdEncoding.EncodeToString([]byte("This is new text")), + }, callback...) +} + +func createGPGKey(tmpDir, name, email string) (*openpgp.Entity, error) { + keyPair, err := openpgp.NewEntity(name, "test", email, nil) + if err != nil { + return nil, err + } + + for _, id := range keyPair.Identities { + err := id.SelfSignature.SignUserId(id.UserId.Id, keyPair.PrimaryKey, keyPair.PrivateKey, nil) + if err != nil { + return nil, err + } + } + + keyFile := filepath.Join(tmpDir, "temporary.key") + keyWriter, err := os.Create(keyFile) + if err != nil { + return nil, err + } + defer keyWriter.Close() + defer os.Remove(keyFile) + + w, err := armor.Encode(keyWriter, openpgp.PrivateKeyType, nil) + if err != nil { + return nil, err + } + defer w.Close() + + keyPair.SerializePrivate(w, nil) + if err := w.Close(); err != nil { + return nil, err + } + if err := keyWriter.Close(); err != nil { + return nil, err + } + + if _, _, err := process.GetManager().Exec("gpg --import temporary.key", "gpg", "--import", keyFile); err != nil { + return nil, err + } + return keyPair, nil +} diff --git a/integrations/issue_test.go b/integrations/issue_test.go index 0b153607ee..aa17c44254 100644 --- a/integrations/issue_test.go +++ b/integrations/issue_test.go @@ -13,6 +13,7 @@ import ( "testing" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" @@ -207,7 +208,7 @@ func TestIssueCrossReference(t *testing.T) { RefIssueID: issueRef.ID, RefCommentID: 0, RefIsPull: false, - RefAction: models.XRefActionNone}) + RefAction: references.XRefActionNone}) // Edit title, neuter ref testIssueChangeInfo(t, "user2", issueRefURL, "title", "Title no ref") @@ -217,7 +218,7 @@ func TestIssueCrossReference(t *testing.T) { RefIssueID: issueRef.ID, RefCommentID: 0, RefIsPull: false, - RefAction: models.XRefActionNeutered}) + RefAction: references.XRefActionNeutered}) // Ref from issue content issueRefURL, issueRef = testIssueWithBean(t, "user2", 1, "TitleXRef", fmt.Sprintf("Description ref #%d", issueBase.Index)) @@ -227,7 +228,7 @@ func TestIssueCrossReference(t *testing.T) { RefIssueID: issueRef.ID, RefCommentID: 0, RefIsPull: false, - RefAction: models.XRefActionNone}) + RefAction: references.XRefActionNone}) // Edit content, neuter ref testIssueChangeInfo(t, "user2", issueRefURL, "content", "Description no ref") @@ -237,7 +238,7 @@ func TestIssueCrossReference(t *testing.T) { RefIssueID: issueRef.ID, RefCommentID: 0, RefIsPull: false, - RefAction: models.XRefActionNeutered}) + RefAction: references.XRefActionNeutered}) // Ref from a comment session := loginUser(t, "user2") @@ -248,7 +249,7 @@ func TestIssueCrossReference(t *testing.T) { RefIssueID: issueRef.ID, RefCommentID: commentID, RefIsPull: false, - RefAction: models.XRefActionNone} + RefAction: references.XRefActionNone} models.AssertExistsAndLoadBean(t, comment) // Ref from a different repository @@ -259,7 +260,7 @@ func TestIssueCrossReference(t *testing.T) { RefIssueID: issueRef.ID, RefCommentID: 0, RefIsPull: false, - RefAction: models.XRefActionNone}) + RefAction: references.XRefActionNone}) } func testIssueWithBean(t *testing.T, user string, repoID int64, title, content string) (string, *models.Issue) { diff --git a/integrations/lfs_getobject_test.go b/integrations/lfs_getobject_test.go index bb269d5eeb..373fffa445 100644 --- a/integrations/lfs_getobject_test.go +++ b/integrations/lfs_getobject_test.go @@ -58,6 +58,11 @@ func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string func doLfs(t *testing.T, content *[]byte, expectGzip bool) { prepareTestEnv(t) + setting.CheckLFSVersion() + if !setting.LFS.StartServer { + t.Skip() + return + } repo, err := models.GetRepositoryByOwnerAndName("user2", "repo1") assert.NoError(t, err) oid := storeObjectInRepo(t, repo.ID, content) diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go index 3b47f0d7fc..8dc366dc3f 100644 --- a/integrations/migration-test/migration_test.go +++ b/integrations/migration-test/migration_test.go @@ -23,8 +23,8 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" "github.com/stretchr/testify/assert" + "xorm.io/xorm" ) var currentEngine *xorm.Engine diff --git a/integrations/mssql.ini.tmpl b/integrations/mssql.ini.tmpl index d38d038a4e..931e923cf4 100644 --- a/integrations/mssql.ini.tmpl +++ b/integrations/mssql.ini.tmpl @@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mssql/gitea-repositories LOCAL_COPY_PATH = tmp/local-repo-mssql LOCAL_WIKI_PATH = tmp/local-wiki-mssql +[repository.signing] +SIGNING_KEY = none + [server] SSH_DOMAIN = localhost HTTP_PORT = 3003 diff --git a/integrations/mysql.ini.tmpl b/integrations/mysql.ini.tmpl index 6eed7e1578..4dde212798 100644 --- a/integrations/mysql.ini.tmpl +++ b/integrations/mysql.ini.tmpl @@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql/gitea-repositories LOCAL_COPY_PATH = tmp/local-repo-mysql LOCAL_WIKI_PATH = tmp/local-wiki-mysql +[repository.signing] +SIGNING_KEY = none + [server] SSH_DOMAIN = localhost HTTP_PORT = 3001 diff --git a/integrations/mysql8.ini.tmpl b/integrations/mysql8.ini.tmpl index 1e14bc1356..1b1d3d2436 100644 --- a/integrations/mysql8.ini.tmpl +++ b/integrations/mysql8.ini.tmpl @@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql8/gitea-repositories LOCAL_COPY_PATH = tmp/local-repo-mysql8 LOCAL_WIKI_PATH = tmp/local-wiki-mysql8 +[repository.signing] +SIGNING_KEY = none + [server] SSH_DOMAIN = localhost HTTP_PORT = 3004 diff --git a/integrations/pgsql.ini.tmpl b/integrations/pgsql.ini.tmpl index cd5dc44ea8..6265e0d98e 100644 --- a/integrations/pgsql.ini.tmpl +++ b/integrations/pgsql.ini.tmpl @@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-pgsql/gitea-repositories LOCAL_COPY_PATH = tmp/local-repo-pgsql LOCAL_WIKI_PATH = tmp/local-wiki-pgsql +[repository.signing] +SIGNING_KEY = none + [server] SSH_DOMAIN = localhost HTTP_PORT = 3002 diff --git a/integrations/repofiles_delete_test.go b/integrations/repofiles_delete_test.go index f4cb4510be..b4c535188b 100644 --- a/integrations/repofiles_delete_test.go +++ b/integrations/repofiles_delete_test.go @@ -53,7 +53,7 @@ func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse { }, Verification: &api.PayloadCommitVerification{ Verified: false, - Reason: "", + Reason: "gpg.error.not_signed_commit", Signature: "", Payload: "", }, diff --git a/integrations/repofiles_update_test.go b/integrations/repofiles_update_test.go index a4ce16d847..c475c70008 100644 --- a/integrations/repofiles_update_test.go +++ b/integrations/repofiles_update_test.go @@ -108,7 +108,7 @@ func getExpectedFileResponseForRepofilesCreate(commitID string) *api.FileRespons }, Verification: &api.PayloadCommitVerification{ Verified: false, - Reason: "unsigned", + Reason: "gpg.error.not_signed_commit", Signature: "", Payload: "", }, @@ -175,7 +175,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename string) *api.F }, Verification: &api.PayloadCommitVerification{ Verified: false, - Reason: "unsigned", + Reason: "gpg.error.not_signed_commit", Signature: "", Payload: "", }, diff --git a/integrations/sqlite.ini b/integrations/sqlite.ini index b188406ee9..de3355c166 100644 --- a/integrations/sqlite.ini +++ b/integrations/sqlite.ini @@ -17,6 +17,9 @@ ROOT = integrations/gitea-integration-sqlite/gitea-repositories LOCAL_COPY_PATH = tmp/local-repo-sqlite LOCAL_WIKI_PATH = tmp/local-wiki-sqlite +[repository.signing] +SIGNING_KEY = none + [server] SSH_DOMAIN = localhost HTTP_PORT = 3003 diff --git a/models/access.go b/models/access.go index 3cdfc62f21..213efe08a6 100644 --- a/models/access.go +++ b/models/access.go @@ -246,6 +246,55 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err return repo.refreshAccesses(e, accessMap) } +// recalculateUserAccess recalculates new access for a single user +// Usable if we know access only affected one user +func (repo *Repository) recalculateUserAccess(e Engine, uid int64) (err error) { + minMode := AccessModeRead + if !repo.IsPrivate { + minMode = AccessModeWrite + } + + accessMode := AccessModeNone + collaborator, err := repo.getCollaboration(e, uid) + if err != nil { + return err + } else if collaborator != nil { + accessMode = collaborator.Mode + } + + if err = repo.getOwner(e); err != nil { + return err + } else if repo.Owner.IsOrganization() { + var teams []Team + if err := e.Join("INNER", "team_repo", "team_repo.team_id = team.id"). + Join("INNER", "team_user", "team_user.team_id = team.id"). + Where("team.org_id = ?", repo.OwnerID). + And("team_repo.repo_id=?", repo.ID). + And("team_user.uid=?", uid). + Find(&teams); err != nil { + return err + } + + for _, t := range teams { + if t.IsOwnerTeam() { + t.Authorize = AccessModeOwner + } + + accessMode = maxAccessMode(accessMode, t.Authorize) + } + } + + // Delete old user accesses and insert new one for repository. + if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: uid}); err != nil { + return fmt.Errorf("delete old user accesses: %v", err) + } else if accessMode >= minMode { + if _, err = e.Insert(&Access{RepoID: repo.ID, UserID: uid, Mode: accessMode}); err != nil { + return fmt.Errorf("insert new user accesses: %v", err) + } + } + return nil +} + func (repo *Repository) recalculateAccesses(e Engine) error { if repo.Owner.IsOrganization() { return repo.recalculateTeamAccesses(e, 0) diff --git a/models/action.go b/models/action.go index 87088101f9..b651c658d5 100644 --- a/models/action.go +++ b/models/action.go @@ -6,19 +6,17 @@ package models import ( - "encoding/json" "fmt" "html" "path" - "regexp" "strconv" "strings" "time" - "unicode" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -54,29 +52,6 @@ const ( ActionMirrorSyncDelete // 20 ) -var ( - // Same as GitHub. See - // https://help.github.com/articles/closing-issues-via-commit-messages - issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"} - issueReopenKeywords = []string{"reopen", "reopens", "reopened"} - - issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp - issueReferenceKeywordsPat *regexp.Regexp -) - -const issueRefRegexpStr = `(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)+` -const issueRefRegexpStrNoKeyword = `(?:\s|^|\(|\[)(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))` - -func assembleKeywordsPattern(words []string) string { - return fmt.Sprintf(`(?i)(?:%s)(?::?) %s`, strings.Join(words, "|"), issueRefRegexpStr) -} - -func init() { - issueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords)) - issueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords)) - issueReferenceKeywordsPat = regexp.MustCompile(issueRefRegexpStrNoKeyword) -} - // Action represents user operation type and other information to // repository. It implemented interface base.Actioner so that can be // used in template render. @@ -351,10 +326,6 @@ func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error return renameRepoAction(x, actUser, oldRepoName, repo) } -func issueIndexTrimRight(c rune) bool { - return !unicode.IsDigit(c) -} - // PushCommit represents a commit in a push operation. type PushCommit struct { Sha1 string @@ -480,39 +451,9 @@ func (pc *PushCommits) AvatarLink(email string) string { } // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue -// if the provided ref is misformatted or references a non-existent issue. -func getIssueFromRef(repo *Repository, ref string) (*Issue, error) { - ref = ref[strings.IndexByte(ref, ' ')+1:] - ref = strings.TrimRightFunc(ref, issueIndexTrimRight) - - var refRepo *Repository - poundIndex := strings.IndexByte(ref, '#') - if poundIndex < 0 { - return nil, nil - } else if poundIndex == 0 { - refRepo = repo - } else { - slashIndex := strings.IndexByte(ref, '/') - if slashIndex < 0 || slashIndex >= poundIndex { - return nil, nil - } - ownerName := ref[:slashIndex] - repoName := ref[slashIndex+1 : poundIndex] - var err error - refRepo, err = GetRepositoryByOwnerAndName(ownerName, repoName) - if err != nil { - if IsErrRepoNotExist(err) { - return nil, nil - } - return nil, err - } - } - issueIndex, err := strconv.ParseInt(ref[poundIndex+1:], 10, 64) - if err != nil { - return nil, nil - } - - issue, err := GetIssueByIndex(refRepo.ID, issueIndex) +// if the provided ref references a non-existent issue. +func getIssueFromRef(repo *Repository, index int64) (*Issue, error) { + issue, err := GetIssueByIndex(repo.ID, index) if err != nil { if IsErrIssueNotExist(err) { return nil, nil @@ -522,20 +463,7 @@ func getIssueFromRef(repo *Repository, ref string) (*Issue, error) { return issue, nil } -func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[int64]bool, status bool) error { - issue, err := getIssueFromRef(repo, ref) - if err != nil { - return err - } - - if issue == nil || refMarked[issue.ID] { - return nil - } - refMarked[issue.ID] = true - - if issue.RepoID != repo.ID || issue.IsClosed == status { - return nil - } +func changeIssueStatus(repo *Repository, issue *Issue, doer *User, status bool) error { stopTimerIfAvailable := func(doer *User, issue *Issue) error { @@ -549,7 +477,7 @@ func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[i } issue.Repo = repo - if err = issue.ChangeStatus(doer, status); err != nil { + if err := issue.ChangeStatus(doer, status); err != nil { // Don't return an error when dependencies are open as this would let the push fail if IsErrDependenciesLeft(err) { return stopTimerIfAvailable(doer, issue) @@ -566,99 +494,67 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit, bra for i := len(commits) - 1; i >= 0; i-- { c := commits[i] - refMarked := make(map[int64]bool) + type markKey struct { + ID int64 + Action references.XRefAction + } + + refMarked := make(map[markKey]bool) var refRepo *Repository + var refIssue *Issue var err error - for _, m := range issueReferenceKeywordsPat.FindAllStringSubmatch(c.Message, -1) { - if len(m[3]) == 0 { - continue - } - ref := m[3] + for _, ref := range references.FindAllIssueReferences(c.Message) { // issue is from another repo - if len(m[1]) > 0 && len(m[2]) > 0 { - refRepo, err = GetRepositoryFromMatch(m[1], m[2]) + if len(ref.Owner) > 0 && len(ref.Name) > 0 { + refRepo, err = GetRepositoryFromMatch(ref.Owner, ref.Name) if err != nil { continue } } else { refRepo = repo } - issue, err := getIssueFromRef(refRepo, ref) - if err != nil { + if refIssue, err = getIssueFromRef(refRepo, ref.Index); err != nil { return err } - - if issue == nil || refMarked[issue.ID] { + if refIssue == nil { continue } - refMarked[issue.ID] = true - - message := fmt.Sprintf(`%s`, repo.Link(), c.Sha1, html.EscapeString(c.Message)) - if err = CreateRefComment(doer, refRepo, issue, message, c.Sha1); err != nil { - return err - } - } - - // Change issue status only if the commit has been pushed to the default branch. - // and if the repo is configured to allow only that - if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch { - continue - } - refMarked = make(map[int64]bool) - for _, m := range issueCloseKeywordsPat.FindAllStringSubmatch(c.Message, -1) { - if len(m[3]) == 0 { - continue - } - ref := m[3] - - // issue is from another repo - if len(m[1]) > 0 && len(m[2]) > 0 { - refRepo, err = GetRepositoryFromMatch(m[1], m[2]) - if err != nil { - continue - } - } else { - refRepo = repo - } perm, err := GetUserRepoPermission(refRepo, doer) if err != nil { return err } - // only close issues in another repo if user has push access - if perm.CanWrite(UnitTypeCode) { - if err := changeIssueStatus(refRepo, doer, ref, refMarked, true); err != nil { + + key := markKey{ID: refIssue.ID, Action: ref.Action} + if refMarked[key] { + continue + } + refMarked[key] = true + + // only create comments for issues if user has permission for it + if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeIssues) { + message := fmt.Sprintf(`%s`, repo.Link(), c.Sha1, html.EscapeString(c.Message)) + if err = CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil { return err } } - } - // It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here. - for _, m := range issueReopenKeywordsPat.FindAllStringSubmatch(c.Message, -1) { - if len(m[3]) == 0 { + // Process closing/reopening keywords + if ref.Action != references.XRefActionCloses && ref.Action != references.XRefActionReopens { continue } - ref := m[3] - // issue is from another repo - if len(m[1]) > 0 && len(m[2]) > 0 { - refRepo, err = GetRepositoryFromMatch(m[1], m[2]) - if err != nil { - continue - } - } else { - refRepo = repo + // Change issue status only if the commit has been pushed to the default branch. + // and if the repo is configured to allow only that + // FIXME: we should be using Issue.ref if set instead of repo.DefaultBranch + if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch { + continue } - perm, err := GetUserRepoPermission(refRepo, doer) - if err != nil { - return err - } - - // only reopen issues in another repo if user has push access - if perm.CanWrite(UnitTypeCode) { - if err := changeIssueStatus(refRepo, doer, ref, refMarked, false); err != nil { + // only close issues in another repo if user has push access + if perm.IsAdmin() || perm.IsOwner() || perm.CanWrite(UnitTypeCode) { + if err := changeIssueStatus(refRepo, refIssue, doer, ref.Action == references.XRefActionCloses); err != nil { return err } } @@ -713,79 +609,6 @@ func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error return mergePullRequestAction(x, actUser, repo, pull) } -func mirrorSyncAction(e Engine, opType ActionType, repo *Repository, refName string, data []byte) error { - if err := notifyWatchers(e, &Action{ - ActUserID: repo.OwnerID, - ActUser: repo.MustOwner(), - OpType: opType, - RepoID: repo.ID, - Repo: repo, - IsPrivate: repo.IsPrivate, - RefName: refName, - Content: string(data), - }); err != nil { - return fmt.Errorf("notifyWatchers: %v", err) - } - - defer func() { - go HookQueue.Add(repo.ID) - }() - - return nil -} - -// MirrorSyncPushActionOptions mirror synchronization action options. -type MirrorSyncPushActionOptions struct { - RefName string - OldCommitID string - NewCommitID string - Commits *PushCommits -} - -// MirrorSyncPushAction adds new action for mirror synchronization of pushed commits. -func MirrorSyncPushAction(repo *Repository, opts MirrorSyncPushActionOptions) error { - if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum { - opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum] - } - - apiCommits, err := opts.Commits.ToAPIPayloadCommits(repo.RepoPath(), repo.HTMLURL()) - if err != nil { - return err - } - - opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID) - apiPusher := repo.MustOwner().APIFormat() - if err := PrepareWebhooks(repo, HookEventPush, &api.PushPayload{ - Ref: opts.RefName, - Before: opts.OldCommitID, - After: opts.NewCommitID, - CompareURL: setting.AppURL + opts.Commits.CompareURL, - Commits: apiCommits, - Repo: repo.APIFormat(AccessModeOwner), - Pusher: apiPusher, - Sender: apiPusher, - }); err != nil { - return fmt.Errorf("PrepareWebhooks: %v", err) - } - - data, err := json.Marshal(opts.Commits) - if err != nil { - return err - } - - return mirrorSyncAction(x, ActionMirrorSyncPush, repo, opts.RefName, data) -} - -// MirrorSyncCreateAction adds new action for mirror synchronization of new reference. -func MirrorSyncCreateAction(repo *Repository, refName string) error { - return mirrorSyncAction(x, ActionMirrorSyncCreate, repo, refName, nil) -} - -// MirrorSyncDeleteAction adds new action for mirror synchronization of delete reference. -func MirrorSyncDeleteAction(repo *Repository, refName string) error { - return mirrorSyncAction(x, ActionMirrorSyncDelete, repo, refName, nil) -} - // GetFeedsOptions options for retrieving feeds type GetFeedsOptions struct { RequestedUser *User diff --git a/models/action_test.go b/models/action_test.go index c90538ebe6..df41556850 100644 --- a/models/action_test.go +++ b/models/action_test.go @@ -1,7 +1,6 @@ package models import ( - "fmt" "path" "strings" "testing" @@ -181,56 +180,6 @@ func TestPushCommits_AvatarLink(t *testing.T) { pushCommits.AvatarLink("nonexistent@example.com")) } -func TestRegExp_issueReferenceKeywordsPat(t *testing.T) { - trueTestCases := []string{ - "#2", - "[#2]", - "please see go-gitea/gitea#5", - "#2:", - } - falseTestCases := []string{ - "kb#2", - "#2xy", - } - - for _, testCase := range trueTestCases { - assert.True(t, issueReferenceKeywordsPat.MatchString(testCase)) - } - for _, testCase := range falseTestCases { - assert.False(t, issueReferenceKeywordsPat.MatchString(testCase)) - } -} - -func Test_getIssueFromRef(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) - for _, test := range []struct { - Ref string - ExpectedIssueID int64 - }{ - {"#2", 2}, - {"reopen #2", 2}, - {"user2/repo2#1", 4}, - {"fixes user2/repo2#1", 4}, - {"fixes: user2/repo2#1", 4}, - } { - issue, err := getIssueFromRef(repo, test.Ref) - assert.NoError(t, err) - if assert.NotNil(t, issue) { - assert.EqualValues(t, test.ExpectedIssueID, issue.ID) - } - } - - for _, badRef := range []string{ - "doesnotexist/doesnotexist#1", - fmt.Sprintf("#%d", NonexistentID), - } { - issue, err := getIssueFromRef(repo, badRef) - assert.NoError(t, err) - assert.Nil(t, issue) - } -} - func TestUpdateIssuesCommit(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) pushCommits := []*PushCommit{ @@ -431,7 +380,7 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) { AssertNotExistsBean(t, commentBean) AssertNotExistsBean(t, issueBean, "is_closed=1") assert.NoError(t, UpdateIssuesCommit(user, repo, pushCommits, repo.DefaultBranch)) - AssertExistsAndLoadBean(t, commentBean) + AssertNotExistsBean(t, commentBean) AssertNotExistsBean(t, issueBean, "is_closed=1") CheckConsistencyFor(t, &Action{}) } diff --git a/models/attachment.go b/models/attachment.go index a9032f1a86..f585bda8cb 100644 --- a/models/attachment.go +++ b/models/attachment.go @@ -14,8 +14,8 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" gouuid "github.com/satori/go.uuid" + "xorm.io/xorm" ) // Attachment represent a attachment of issue/comment/release. diff --git a/models/branches.go b/models/branches.go index 9daaa487e7..c5f227f1e5 100644 --- a/models/branches.go +++ b/models/branches.go @@ -34,6 +34,7 @@ type ProtectedBranch struct { WhitelistUserIDs []int64 `xorm:"JSON TEXT"` WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"` + WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"` MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` @@ -195,7 +196,7 @@ func UpdateProtectBranch(repo *Repository, protectBranch *ProtectedBranch, opts } protectBranch.MergeWhitelistUserIDs = whitelist - whitelist, err = updateUserWhitelist(repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs) + whitelist, err = updateApprovalWhitelist(repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs) if err != nil { return err } @@ -301,6 +302,27 @@ func (repo *Repository) IsProtectedBranchForMerging(pr *PullRequest, branchName return false, nil } +// updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with +// the users from newWhitelist which have explicit read or write access to the repo. +func updateApprovalWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { + hasUsersChanged := !util.IsSliceInt64Eq(currentWhitelist, newWhitelist) + if !hasUsersChanged { + return currentWhitelist, nil + } + + whitelist = make([]int64, 0, len(newWhitelist)) + for _, userID := range newWhitelist { + if reader, err := repo.IsReader(userID); err != nil { + return nil, err + } else if !reader { + continue + } + whitelist = append(whitelist, userID) + } + + return +} + // updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with // the users from newWhitelist which have write access to the repo. func updateUserWhitelist(repo *Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { diff --git a/models/commit_status.go b/models/commit_status.go index 6f6cbc387f..4e0f8166f3 100644 --- a/models/commit_status.go +++ b/models/commit_status.go @@ -16,7 +16,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // CommitStatusState holds the state of a Status diff --git a/models/external_login_user.go b/models/external_login_user.go index 21a3cbbd31..265d855ccf 100644 --- a/models/external_login_user.go +++ b/models/external_login_user.go @@ -4,13 +4,34 @@ package models -import "github.com/markbates/goth" +import ( + "time" + + "code.gitea.io/gitea/modules/structs" + + "github.com/markbates/goth" + "xorm.io/builder" +) // ExternalLoginUser makes the connecting between some existing user and additional external login sources type ExternalLoginUser struct { - ExternalID string `xorm:"pk NOT NULL"` - UserID int64 `xorm:"INDEX NOT NULL"` - LoginSourceID int64 `xorm:"pk NOT NULL"` + ExternalID string `xorm:"pk NOT NULL"` + UserID int64 `xorm:"INDEX NOT NULL"` + LoginSourceID int64 `xorm:"pk NOT NULL"` + RawData map[string]interface{} `xorm:"TEXT JSON"` + Provider string `xorm:"index VARCHAR(25)"` + Email string + Name string + FirstName string + LastName string + NickName string + Description string + AvatarURL string + Location string + AccessToken string `xorm:"TEXT"` + AccessTokenSecret string `xorm:"TEXT"` + RefreshToken string `xorm:"TEXT"` + ExpiresAt time.Time } // GetExternalLogin checks if a externalID in loginSourceID scope already exists @@ -32,23 +53,15 @@ func ListAccountLinks(user *User) ([]*ExternalLoginUser, error) { return externalAccounts, nil } -// LinkAccountToUser link the gothUser to the user -func LinkAccountToUser(user *User, gothUser goth.User) error { - loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider) - if err != nil { - return err - } - - externalLoginUser := &ExternalLoginUser{ - ExternalID: gothUser.UserID, - UserID: user.ID, - LoginSourceID: loginSource.ID, - } - has, err := x.Get(externalLoginUser) +// LinkExternalToUser link the external user to the user +func LinkExternalToUser(user *User, externalLoginUser *ExternalLoginUser) error { + has, err := x.Where("external_id=? AND login_source_id=?", externalLoginUser.ExternalID, externalLoginUser.LoginSourceID). + NoAutoCondition(). + Exist(externalLoginUser) if err != nil { return err } else if has { - return ErrExternalLoginUserAlreadyExist{gothUser.UserID, user.ID, loginSource.ID} + return ErrExternalLoginUserAlreadyExist{externalLoginUser.ExternalID, user.ID, externalLoginUser.LoginSourceID} } _, err = x.Insert(externalLoginUser) @@ -72,3 +85,97 @@ func removeAllAccountLinks(e Engine, user *User) error { _, err := e.Delete(&ExternalLoginUser{UserID: user.ID}) return err } + +// GetUserIDByExternalUserID get user id according to provider and userID +func GetUserIDByExternalUserID(provider string, userID string) (int64, error) { + var id int64 + _, err := x.Table("external_login_user"). + Select("user_id"). + Where("provider=?", provider). + And("external_id=?", userID). + Get(&id) + if err != nil { + return 0, err + } + return id, nil +} + +// UpdateExternalUser updates external user's information +func UpdateExternalUser(user *User, gothUser goth.User) error { + loginSource, err := GetActiveOAuth2LoginSourceByName(gothUser.Provider) + if err != nil { + return err + } + externalLoginUser := &ExternalLoginUser{ + ExternalID: gothUser.UserID, + UserID: user.ID, + LoginSourceID: loginSource.ID, + RawData: gothUser.RawData, + Provider: gothUser.Provider, + Email: gothUser.Email, + Name: gothUser.Name, + FirstName: gothUser.FirstName, + LastName: gothUser.LastName, + NickName: gothUser.NickName, + Description: gothUser.Description, + AvatarURL: gothUser.AvatarURL, + Location: gothUser.Location, + AccessToken: gothUser.AccessToken, + AccessTokenSecret: gothUser.AccessTokenSecret, + RefreshToken: gothUser.RefreshToken, + ExpiresAt: gothUser.ExpiresAt, + } + + has, err := x.Where("external_id=? AND login_source_id=?", gothUser.UserID, loginSource.ID). + NoAutoCondition(). + Exist(externalLoginUser) + if err != nil { + return err + } else if !has { + return ErrExternalLoginUserNotExist{user.ID, loginSource.ID} + } + + _, err = x.Where("external_id=? AND login_source_id=?", gothUser.UserID, loginSource.ID).AllCols().Update(externalLoginUser) + return err +} + +// FindExternalUserOptions represents an options to find external users +type FindExternalUserOptions struct { + Provider string + Limit int + Start int +} + +func (opts FindExternalUserOptions) toConds() builder.Cond { + var cond = builder.NewCond() + if len(opts.Provider) > 0 { + cond = cond.And(builder.Eq{"provider": opts.Provider}) + } + return cond +} + +// FindExternalUsersByProvider represents external users via provider +func FindExternalUsersByProvider(opts FindExternalUserOptions) ([]ExternalLoginUser, error) { + var users []ExternalLoginUser + err := x.Where(opts.toConds()). + Limit(opts.Limit, opts.Start). + OrderBy("login_source_id ASC, external_id ASC"). + Find(&users) + if err != nil { + return nil, err + } + return users, nil +} + +// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID +func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error { + if err := UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + + if err := UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + + return UpdateReleasesMigrationsByType(tp, externalUserID, userID) +} diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml index 34a1a8b2be..e8a6d531f2 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -5,7 +5,7 @@ act_user_id: 2 repo_id: 2 is_private: true - created_unix: 1540139562 + created_unix: 1571686356 - id: 2 diff --git a/models/fixtures/pull_request.yml b/models/fixtures/pull_request.yml index baaaf6bb8a..505584ea18 100644 --- a/models/fixtures/pull_request.yml +++ b/models/fixtures/pull_request.yml @@ -6,7 +6,6 @@ index: 2 head_repo_id: 1 base_repo_id: 1 - head_user_name: user1 head_branch: branch1 base_branch: master merge_base: 1234567890abcdef @@ -21,7 +20,6 @@ index: 3 head_repo_id: 1 base_repo_id: 1 - head_user_name: user1 head_branch: branch2 base_branch: master merge_base: fedcba9876543210 @@ -35,7 +33,6 @@ index: 1 head_repo_id: 11 base_repo_id: 10 - head_user_name: user13 head_branch: branch2 base_branch: master merge_base: 0abcb056019adb83 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 2e38c5e1dd..cf7d24c6cd 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -11,6 +11,7 @@ num_milestones: 3 num_closed_milestones: 1 num_watches: 3 + status: 0 - id: 2 @@ -24,6 +25,7 @@ num_closed_pulls: 0 num_stars: 1 close_issues_via_commit_in_any_branch: true + status: 0 - id: 3 @@ -36,6 +38,7 @@ num_pulls: 0 num_closed_pulls: 0 num_watches: 0 + status: 0 - id: 4 @@ -48,6 +51,7 @@ num_pulls: 0 num_closed_pulls: 0 num_stars: 1 + status: 0 - id: 5 @@ -61,6 +65,7 @@ num_closed_pulls: 0 num_watches: 0 is_mirror: true + status: 0 - id: 6 @@ -73,6 +78,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 7 @@ -85,6 +91,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 8 @@ -97,6 +104,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 9 @@ -109,6 +117,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 10 @@ -122,6 +131,7 @@ num_closed_pulls: 0 is_mirror: false num_forks: 1 + status: 0 - id: 11 @@ -135,6 +145,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 12 @@ -147,6 +158,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 13 @@ -159,6 +171,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 14 @@ -172,6 +185,7 @@ num_pulls: 0 num_closed_pulls: 0 is_mirror: false + status: 0 - id: 15 @@ -179,6 +193,7 @@ lower_name: repo15 name: repo15 is_empty: true + status: 0 - id: 16 @@ -191,6 +206,7 @@ num_pulls: 0 num_closed_pulls: 0 num_watches: 0 + status: 0 - id: 17 @@ -205,6 +221,7 @@ num_watches: 0 is_mirror: false is_fork: false + status: 0 - id: 18 @@ -218,6 +235,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 19 @@ -231,6 +249,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 20 @@ -244,6 +263,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 21 @@ -257,6 +277,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 22 @@ -270,6 +291,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 23 @@ -283,6 +305,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 24 @@ -296,6 +319,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: false + status: 0 - id: 25 @@ -310,6 +334,7 @@ num_watches: 0 is_mirror: true is_fork: false + status: 0 - id: 26 @@ -324,6 +349,7 @@ num_watches: 0 is_mirror: true is_fork: false + status: 0 - id: 27 @@ -339,6 +365,7 @@ is_mirror: true num_forks: 1 is_fork: false + status: 0 - id: 28 @@ -354,6 +381,7 @@ is_mirror: true num_forks: 1 is_fork: false + status: 0 - id: 29 @@ -368,6 +396,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: true + status: 0 - id: 30 @@ -382,6 +411,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: true + status: 0 - id: 31 @@ -392,6 +422,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 32 # org public repo @@ -403,6 +434,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 33 @@ -410,6 +442,7 @@ lower_name: utf8 name: utf8 is_private: false + status: 0 - id: 34 @@ -421,6 +454,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 35 @@ -432,6 +466,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 36 @@ -443,6 +478,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 37 @@ -454,6 +490,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 38 @@ -465,6 +502,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 39 @@ -476,6 +514,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 40 @@ -487,6 +526,7 @@ num_forks: 0 num_issues: 0 is_mirror: false + status: 0 - id: 41 @@ -519,4 +559,5 @@ num_stars: 0 num_forks: 0 num_issues: 0 - is_mirror: false \ No newline at end of file + is_mirror: false + status: 0 diff --git a/models/gpg_key.go b/models/gpg_key.go index 72c6891d4d..58eaa61e22 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -17,12 +17,13 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "github.com/keybase/go-crypto/openpgp" "github.com/keybase/go-crypto/openpgp/armor" "github.com/keybase/go-crypto/openpgp/packet" + "xorm.io/xorm" ) // GPGKey represents a GPG key. @@ -80,6 +81,12 @@ func GetGPGKeyByID(keyID int64) (*GPGKey, error) { return key, nil } +// GetGPGKeysByKeyID returns public key by given ID. +func GetGPGKeysByKeyID(keyID string) ([]*GPGKey, error) { + keys := make([]*GPGKey, 0, 1) + return keys, x.Where("key_id=?", keyID).Find(&keys) +} + // GetGPGImportByKeyID returns the import public armored key by given KeyID. func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) { key := new(GPGKeyImport) @@ -355,10 +362,13 @@ func DeleteGPGKey(doer *User, id int64) (err error) { // CommitVerification represents a commit validation of signature type CommitVerification struct { - Verified bool - Reason string - SigningUser *User - SigningKey *GPGKey + Verified bool + Warning bool + Reason string + SigningUser *User + CommittingUser *User + SigningEmail string + SigningKey *GPGKey } // SignCommit represents a commit with validation of signature. @@ -367,6 +377,17 @@ type SignCommit struct { *UserCommit } +const ( + // BadSignature is used as the reason when the signature has a KeyID that is in the db + // but no key that has that ID verifies the signature. This is a suspicious failure. + BadSignature = "gpg.error.probable_bad_signature" + // BadDefaultSignature is used as the reason when the signature has a KeyID that matches the + // default Key but is not verified by the default key. This is a suspicious failure. + BadDefaultSignature = "gpg.error.probable_bad_default_signature" + // NoKeyFound is used as the reason when no key can be found to verify the signature. + NoKeyFound = "gpg.error.no_gpg_keys_found" +) + func readerFromBase64(s string) (io.Reader, error) { bs, err := base64.StdEncoding.DecodeString(s) if err != nil { @@ -424,49 +445,207 @@ func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { return pkey.VerifySignature(h, s) } -// ParseCommitWithSignature check if signature is good against keystore. -func ParseCommitWithSignature(c *git.Commit) *CommitVerification { - if c.Signature != nil && c.Committer != nil { - //Parsing signature - sig, err := extractSignature(c.Signature.Signature) - if err != nil { //Skipping failed to extract sign - log.Error("SignatureRead err: %v", err) - return &CommitVerification{ - Verified: false, - Reason: "gpg.error.extract_sign", +func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification { + //Generating hash of commit + hash, err := populateHash(sig.Hash, []byte(payload)) + if err != nil { //Skipping failed to generate hash + log.Error("PopulateHash: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.generate_hash", + } + } + + if err := verifySign(sig, hash, k); err == nil { + return &CommitVerification{ //Everything is ok + CommittingUser: committer, + Verified: true, + Reason: fmt.Sprintf("%s <%s> / %s", signer.Name, signer.Email, k.KeyID), + SigningUser: signer, + SigningKey: k, + SigningEmail: email, + } + } + return nil +} + +func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification { + commitVerification := hashAndVerify(sig, payload, k, committer, signer, email) + if commitVerification != nil { + return commitVerification + } + + //And test also SubsKey + for _, sk := range k.SubsKey { + commitVerification := hashAndVerify(sig, payload, sk, committer, signer, email) + if commitVerification != nil { + return commitVerification + } + } + return nil +} + +func hashAndVerifyForKeyID(sig *packet.Signature, payload string, committer *User, keyID, name, email string) *CommitVerification { + if keyID == "" { + return nil + } + keys, err := GetGPGKeysByKeyID(keyID) + if err != nil { + log.Error("GetGPGKeysByKeyID: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.failed_retrieval_gpg_keys", + } + } + if len(keys) == 0 { + return nil + } + for _, key := range keys { + activated := false + if len(email) != 0 { + for _, e := range key.Emails { + if e.IsActivated && strings.EqualFold(e.Email, email) { + activated = true + email = e.Email + break + } + } + } else { + for _, e := range key.Emails { + if e.IsActivated { + activated = true + email = e.Email + break + } } } + if !activated { + continue + } + signer := &User{ + Name: name, + Email: email, + } + if key.OwnerID != 0 { + owner, err := GetUserByID(key.OwnerID) + if err == nil { + signer = owner + } else if !IsErrUserNotExist(err) { + log.Error("Failed to GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.no_committer_account", + } + } + } + commitVerification := hashAndVerifyWithSubKeys(sig, payload, key, committer, signer, email) + if commitVerification != nil { + return commitVerification + } + } + // This is a bad situation ... We have a key id that is in our database but the signature doesn't match. + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Warning: true, + Reason: BadSignature, + } +} +// ParseCommitWithSignature check if signature is good against keystore. +func ParseCommitWithSignature(c *git.Commit) *CommitVerification { + var committer *User + if c.Committer != nil { + var err error //Find Committer account - committer, err := GetUserByEmail(c.Committer.Email) //This find the user by primary email or activated email so commit will not be valid if email is not - if err != nil { //Skipping not user for commiter + committer, err = GetUserByEmail(c.Committer.Email) //This finds the user by primary email or activated email so commit will not be valid if email is not + if err != nil { //Skipping not user for commiter + committer = &User{ + Name: c.Committer.Name, + Email: c.Committer.Email, + } // We can expect this to often be an ErrUserNotExist. in the case // it is not, however, it is important to log it. if !IsErrUserNotExist(err) { log.Error("GetUserByEmail: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.no_committer_account", + } } - return &CommitVerification{ - Verified: false, - Reason: "gpg.error.no_committer_account", - } - } + } + } + + // If no signature just report the committer + if c.Signature == nil { + return &CommitVerification{ + CommittingUser: committer, + Verified: false, //Default value + Reason: "gpg.error.not_signed_commit", //Default value + } + } + + //Parsing signature + sig, err := extractSignature(c.Signature.Signature) + if err != nil { //Skipping failed to extract sign + log.Error("SignatureRead err: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.extract_sign", + } + } + + keyID := "" + if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { + keyID = fmt.Sprintf("%X", *sig.IssuerKeyId) + } + if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 { + keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20]) + } + + defaultReason := NoKeyFound + + // First check if the sig has a keyID and if so just look at that + if commitVerification := hashAndVerifyForKeyID( + sig, + c.Signature.Payload, + committer, + keyID, + setting.AppName, + ""); commitVerification != nil { + if commitVerification.Reason == BadSignature { + defaultReason = BadSignature + } else { + return commitVerification + } + } + + // Now try to associate the signature with the committer, if present + if committer.ID != 0 { keys, err := ListGPGKeys(committer.ID) if err != nil { //Skipping failed to get gpg keys of user log.Error("ListGPGKeys: %v", err) return &CommitVerification{ - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.failed_retrieval_gpg_keys", } } for _, k := range keys { //Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate canValidate := false - lowerCommiterEmail := strings.ToLower(c.Committer.Email) + email := "" for _, e := range k.Emails { - if e.IsActivated && strings.ToLower(e.Email) == lowerCommiterEmail { + if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { canValidate = true + email = e.Email break } } @@ -474,56 +653,104 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification { continue //Skip this key } - //Generating hash of commit - hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload)) - if err != nil { //Skipping ailed to generate hash - log.Error("PopulateHash: %v", err) - return &CommitVerification{ - Verified: false, - Reason: "gpg.error.generate_hash", - } + commitVerification := hashAndVerifyWithSubKeys(sig, c.Signature.Payload, k, committer, committer, email) + if commitVerification != nil { + return commitVerification } - //We get PK - if err := verifySign(sig, hash, k); err == nil { - return &CommitVerification{ //Everything is ok - Verified: true, - Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, k.KeyID), - SigningUser: committer, - SigningKey: k, - } - } - //And test also SubsKey - for _, sk := range k.SubsKey { - - //Generating hash of commit - hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload)) - if err != nil { //Skipping ailed to generate hash - log.Error("PopulateHash: %v", err) - return &CommitVerification{ - Verified: false, - Reason: "gpg.error.generate_hash", - } - } - if err := verifySign(sig, hash, sk); err == nil { - return &CommitVerification{ //Everything is ok - Verified: true, - Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID), - SigningUser: committer, - SigningKey: sk, - } - } - } - } - return &CommitVerification{ //Default at this stage - Verified: false, - Reason: "gpg.error.no_gpg_keys_found", } } - return &CommitVerification{ - Verified: false, //Default value - Reason: "gpg.error.not_signed_commit", //Default value + if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { + // OK we should try the default key + gpgSettings := git.GPGSettings{ + Sign: true, + KeyID: setting.Repository.Signing.SigningKey, + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } + if err := gpgSettings.LoadPublicKeyContent(); err != nil { + log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) + } else if commitVerification := verifyWithGPGSettings(&gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { + if commitVerification.Reason == BadSignature { + defaultReason = BadSignature + } else { + return commitVerification + } + } } + + defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) + if err != nil { + log.Error("Error getting default public gpg key: %v", err) + } else if defaultGPGSettings == nil { + log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) + } else if defaultGPGSettings.Sign { + if commitVerification := verifyWithGPGSettings(defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { + if commitVerification.Reason == BadSignature { + defaultReason = BadSignature + } else { + return commitVerification + } + } + } + + return &CommitVerification{ //Default at this stage + CommittingUser: committer, + Verified: false, + Warning: defaultReason != NoKeyFound, + Reason: defaultReason, + SigningKey: &GPGKey{ + KeyID: keyID, + }, + } +} + +func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *User, keyID string) *CommitVerification { + // First try to find the key in the db + if commitVerification := hashAndVerifyForKeyID(sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { + return commitVerification + } + + // Otherwise we have to parse the key + ekey, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent) + if err != nil { + log.Error("Unable to get default signing key: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.generate_hash", + } + } + pubkey := ekey.PrimaryKey + content, err := base64EncPubKey(pubkey) + if err != nil { + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.generate_hash", + } + } + k := &GPGKey{ + Content: content, + CanSign: pubkey.CanSign(), + KeyID: pubkey.KeyIdString(), + } + if commitVerification := hashAndVerifyWithSubKeys(sig, payload, k, committer, &User{ + Name: gpgSettings.Name, + Email: gpgSettings.Email, + }, gpgSettings.Email); commitVerification != nil { + return commitVerification + } + if keyID == k.KeyID { + // This is a bad situation ... We have a key id that matches our default key but the signature doesn't match. + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Warning: true, + Reason: BadSignature, + } + } + return nil } // ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys. diff --git a/models/graph.go b/models/graph.go index 5f68abaf74..0efb51b3fc 100644 --- a/models/graph.go +++ b/models/graph.go @@ -30,7 +30,7 @@ type GraphItem struct { type GraphItems []GraphItem // GetCommitGraph return a list of commit (GraphItems) from all branches -func GetCommitGraph(r *git.Repository) (GraphItems, error) { +func GetCommitGraph(r *git.Repository, page int) (GraphItems, error) { var CommitGraph []GraphItem @@ -43,6 +43,7 @@ func GetCommitGraph(r *git.Repository) (GraphItems, error) { "-C", "-M", fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum), + fmt.Sprintf("--skip=%d", setting.UI.GraphMaxCommitNum*(page-1)), "--date=iso", fmt.Sprintf("--pretty=format:%s", format), ) diff --git a/models/graph_test.go b/models/graph_test.go index 5c78e3877b..c1f0bc90d9 100644 --- a/models/graph_test.go +++ b/models/graph_test.go @@ -19,7 +19,7 @@ func BenchmarkGetCommitGraph(b *testing.B) { } for i := 0; i < b.N; i++ { - graph, err := GetCommitGraph(currentRepo) + graph, err := GetCommitGraph(currentRepo, 1) if err != nil { b.Error("Could get commit graph") } diff --git a/models/issue.go b/models/issue.go index 9590bc04ff..0315580c31 100644 --- a/models/issue.go +++ b/models/issue.go @@ -14,13 +14,14 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "github.com/go-xorm/xorm" "github.com/unknwon/com" "xorm.io/builder" + "xorm.io/xorm" ) // Issue represents an issue or pull request of repository. @@ -32,7 +33,7 @@ type Issue struct { PosterID int64 `xorm:"INDEX"` Poster *User `xorm:"-"` OriginalAuthor string - OriginalAuthorID int64 + OriginalAuthorID int64 `xorm:"index"` Title string `xorm:"name"` Content string `xorm:"TEXT"` RenderedContent string `xorm:"-"` @@ -427,52 +428,6 @@ func (issue *Issue) HasLabel(labelID int64) bool { return issue.hasLabel(x, labelID) } -func (issue *Issue) sendLabelUpdatedWebhook(doer *User) { - var err error - - if err = issue.loadRepo(x); err != nil { - log.Error("loadRepo: %v", err) - return - } - - if err = issue.loadPoster(x); err != nil { - log.Error("loadPoster: %v", err) - return - } - - mode, _ := AccessLevel(issue.Poster, issue.Repo) - if issue.IsPull { - if err = issue.loadPullRequest(x); err != nil { - log.Error("loadPullRequest: %v", err) - return - } - if err = issue.PullRequest.LoadIssue(); err != nil { - log.Error("LoadIssue: %v", err) - return - } - err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{ - Action: api.HookIssueLabelUpdated, - Index: issue.Index, - PullRequest: issue.PullRequest.APIFormat(), - Repository: issue.Repo.APIFormat(AccessModeNone), - Sender: doer.APIFormat(), - }) - } else { - err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{ - Action: api.HookIssueLabelUpdated, - Index: issue.Index, - Issue: issue.APIFormat(), - Repository: issue.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }) - } - if err != nil { - log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go HookQueue.Add(issue.RepoID) - } -} - // ReplyReference returns tokenized address to use for email reply headers func (issue *Issue) ReplyReference() string { var path string @@ -489,30 +444,10 @@ func (issue *Issue) addLabel(e *xorm.Session, label *Label, doer *User) error { return newIssueLabel(e, issue, label, doer) } -// AddLabel adds a new label to the issue. -func (issue *Issue) AddLabel(doer *User, label *Label) error { - if err := NewIssueLabel(issue, label, doer); err != nil { - return err - } - - issue.sendLabelUpdatedWebhook(doer) - return nil -} - func (issue *Issue) addLabels(e *xorm.Session, labels []*Label, doer *User) error { return newIssueLabels(e, issue, labels, doer) } -// AddLabels adds a list of new labels to the issue. -func (issue *Issue) AddLabels(doer *User, labels []*Label) error { - if err := NewIssueLabels(issue, labels, doer); err != nil { - return err - } - - issue.sendLabelUpdatedWebhook(doer) - return nil -} - func (issue *Issue) getLabels(e Engine) (err error) { if len(issue.Labels) > 0 { return nil @@ -529,28 +464,6 @@ func (issue *Issue) removeLabel(e *xorm.Session, doer *User, label *Label) error return deleteIssueLabel(e, issue, label, doer) } -// RemoveLabel removes a label from issue by given ID. -func (issue *Issue) RemoveLabel(doer *User, label *Label) error { - if err := issue.loadRepo(x); err != nil { - return err - } - - perm, err := GetUserRepoPermission(issue.Repo, doer) - if err != nil { - return err - } - if !perm.CanWriteIssuesOrPulls(issue.IsPull) { - return ErrLabelNotExist{} - } - - if err := DeleteIssueLabel(issue, label, doer); err != nil { - return err - } - - issue.sendLabelUpdatedWebhook(doer) - return nil -} - func (issue *Issue) clearLabels(e *xorm.Session, doer *User) (err error) { if err = issue.getLabels(e); err != nil { return fmt.Errorf("getLabels: %v", err) @@ -595,40 +508,6 @@ func (issue *Issue) ClearLabels(doer *User) (err error) { if err = sess.Commit(); err != nil { return fmt.Errorf("Commit: %v", err) } - sess.Close() - - if err = issue.LoadPoster(); err != nil { - return fmt.Errorf("loadPoster: %v", err) - } - - mode, _ := AccessLevel(issue.Poster, issue.Repo) - if issue.IsPull { - err = issue.PullRequest.LoadIssue() - if err != nil { - log.Error("LoadIssue: %v", err) - return - } - err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{ - Action: api.HookIssueLabelCleared, - Index: issue.Index, - PullRequest: issue.PullRequest.APIFormat(), - Repository: issue.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }) - } else { - err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{ - Action: api.HookIssueLabelCleared, - Index: issue.Index, - Issue: issue.APIFormat(), - Repository: issue.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }) - } - if err != nil { - log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go HookQueue.Add(issue.RepoID) - } return nil } @@ -714,11 +593,6 @@ func updateIssueCols(e Engine, issue *Issue, cols ...string) error { return nil } -// UpdateIssueCols only updates values of specific columns for given issue. -func UpdateIssueCols(issue *Issue, cols ...string) error { - return updateIssueCols(x, issue, cols...) -} - func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (err error) { // Reload the issue currentIssue, err := getIssueByID(e, issue.ID) @@ -766,7 +640,7 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (er } // Update issue count of milestone - if err = changeMilestoneIssueStats(e, issue); err != nil { + if err := updateMilestoneClosedNum(e, issue.MilestoneID); err != nil { return err } @@ -844,9 +718,7 @@ func (issue *Issue) ChangeStatus(doer *User, isClosed bool) (err error) { } // ChangeTitle changes the title of this issue, as the given user. -func (issue *Issue) ChangeTitle(doer *User, title string) (err error) { - oldTitle := issue.Title - issue.Title = title +func (issue *Issue) ChangeTitle(doer *User, oldTitle string) (err error) { sess := x.NewSession() defer sess.Close() @@ -862,7 +734,7 @@ func (issue *Issue) ChangeTitle(doer *User, title string) (err error) { return fmt.Errorf("loadRepo: %v", err) } - if _, err = createChangeTitleComment(sess, doer, issue.Repo, issue, oldTitle, title); err != nil { + if _, err = createChangeTitleComment(sess, doer, issue.Repo, issue, oldTitle, issue.Title); err != nil { return fmt.Errorf("createChangeTitleComment: %v", err) } @@ -874,51 +746,7 @@ func (issue *Issue) ChangeTitle(doer *User, title string) (err error) { return err } - if err = sess.Commit(); err != nil { - return err - } - sess.Close() - - mode, _ := AccessLevel(issue.Poster, issue.Repo) - if issue.IsPull { - if err = issue.loadPullRequest(sess); err != nil { - return fmt.Errorf("loadPullRequest: %v", err) - } - issue.PullRequest.Issue = issue - err = PrepareWebhooks(issue.Repo, HookEventPullRequest, &api.PullRequestPayload{ - Action: api.HookIssueEdited, - Index: issue.Index, - Changes: &api.ChangesPayload{ - Title: &api.ChangesFromPayload{ - From: oldTitle, - }, - }, - PullRequest: issue.PullRequest.APIFormat(), - Repository: issue.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }) - } else { - err = PrepareWebhooks(issue.Repo, HookEventIssues, &api.IssuePayload{ - Action: api.HookIssueEdited, - Index: issue.Index, - Changes: &api.ChangesPayload{ - Title: &api.ChangesFromPayload{ - From: oldTitle, - }, - }, - Issue: issue.APIFormat(), - Repository: issue.Repo.APIFormat(mode), - Sender: issue.Poster.APIFormat(), - }) - } - - if err != nil { - log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go HookQueue.Add(issue.RepoID) - } - - return nil + return sess.Commit() } // AddDeletePRBranchComment adds delete branch comment for pull request issue @@ -939,6 +767,26 @@ func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branc return sess.Commit() } +// UpdateAttachments update attachments by UUIDs for the issue +func (issue *Issue) UpdateAttachments(uuids []string) (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + attachments, err := getAttachmentsByUUIDs(sess, uuids) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err) + } + for i := 0; i < len(attachments); i++ { + attachments[i].IssueID = issue.ID + if err := updateAttachment(sess, attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err) + } + } + return sess.Commit() +} + // ChangeContent changes issue content, as the given user. func (issue *Issue) ChangeContent(doer *User, content string) (err error) { oldContent := issue.Content @@ -1048,7 +896,6 @@ type NewIssueOptions struct { Repo *Repository Issue *Issue LabelIDs []int64 - AssigneeIDs []int64 Attachments []string // In UUID format. IsPull bool } @@ -1070,40 +917,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { } } - // Keep the old assignee id thingy for compatibility reasons - if opts.Issue.AssigneeID > 0 { - isAdded := false - // Check if the user has already been passed to issue.AssigneeIDs, if not, add it - for _, aID := range opts.AssigneeIDs { - if aID == opts.Issue.AssigneeID { - isAdded = true - break - } - } - - if !isAdded { - opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID) - } - } - - // Check for and validate assignees - if len(opts.AssigneeIDs) > 0 { - for _, assigneeID := range opts.AssigneeIDs { - user, err := getUserByID(e, assigneeID) - if err != nil { - return fmt.Errorf("getUserByID [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err) - } - valid, err := canBeAssigned(e, user, opts.Repo) - if err != nil { - return fmt.Errorf("canBeAssigned [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err) - } - if !valid { - return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name} - } - } - } - - // Milestone and assignee validation should happen before insert actual object. + // Milestone validation should happen before insert actual object. if _, err := e.SetExpr("`index`", "coalesce(MAX(`index`),0)+1"). Where("repo_id=?", opts.Issue.RepoID). Insert(opts.Issue); err != nil { @@ -1119,15 +933,11 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { opts.Issue.Index = inserted.Index if opts.Issue.MilestoneID > 0 { - if err = changeMilestoneAssign(e, doer, opts.Issue, -1); err != nil { + if _, err = e.Exec("UPDATE `milestone` SET num_issues=num_issues+1 WHERE id=?", opts.Issue.MilestoneID); err != nil { return err } - } - // Insert the assignees - for _, assigneeID := range opts.AssigneeIDs { - err = opts.Issue.changeAssignee(e, doer, assigneeID, true) - if err != nil { + if _, err = createMilestoneComment(e, doer, opts.Repo, opts.Issue, 0, opts.Issue.MilestoneID); err != nil { return err } } @@ -1189,11 +999,11 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { } // NewIssue creates new issue with labels for repository. -func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) { +func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { // Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887 i := 0 for { - if err = newIssueAttempt(repo, issue, labelIDs, assigneeIDs, uuids); err == nil { + if err = newIssueAttempt(repo, issue, labelIDs, uuids); err == nil { return nil } if !IsErrNewIssueInsert(err) { @@ -1207,7 +1017,7 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []in return fmt.Errorf("NewIssue: too many errors attempting to insert the new issue. Last error was: %v", err) } -func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) { +func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -1219,7 +1029,6 @@ func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, assigneeI Issue: issue, LabelIDs: labelIDs, Attachments: uuids, - AssigneeIDs: assigneeIDs, }); err != nil { if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { return err @@ -1400,8 +1209,12 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { if opts.LabelIDs != nil { for i, labelID := range opts.LabelIDs { - sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), - fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) + if labelID > 0 { + sess.Join("INNER", fmt.Sprintf("issue_label il%d", i), + fmt.Sprintf("issue.id = il%[1]d.issue_id AND il%[1]d.label_id = %[2]d", i, labelID)) + } else { + sess.Where("issue.id not in (select issue_id from issue_label where label_id = ?)", -labelID) + } } } } @@ -1477,46 +1290,18 @@ func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) { return users, e.In("id", userIDs).Find(&users) } -// UpdateIssueMentions extracts mentioned people from content and -// updates issue-user relations for them. -func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []string) error { +// UpdateIssueMentions updates issue-user relations for mentioned users. +func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []*User) error { if len(mentions) == 0 { return nil } - - for i := range mentions { - mentions[i] = strings.ToLower(mentions[i]) + ids := make([]int64, len(mentions)) + for i, u := range mentions { + ids[i] = u.ID } - users := make([]*User, 0, len(mentions)) - - if err := ctx.e.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil { - return fmt.Errorf("find mentioned users: %v", err) - } - - ids := make([]int64, 0, len(mentions)) - for _, user := range users { - ids = append(ids, user.ID) - if !user.IsOrganization() || user.NumMembers == 0 { - continue - } - - memberIDs := make([]int64, 0, user.NumMembers) - orgUsers, err := getOrgUsersByOrgID(ctx.e, user.ID) - if err != nil { - return fmt.Errorf("GetOrgUsersByOrgID [%d]: %v", user.ID, err) - } - - for _, orgUser := range orgUsers { - memberIDs = append(memberIDs, orgUser.ID) - } - - ids = append(ids, memberIDs...) - } - if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil { return fmt.Errorf("UpdateIssueUsersByMentions: %v", err) } - return nil } @@ -1909,3 +1694,133 @@ func (issue *Issue) updateClosedNum(e Engine) (err error) { } return } + +// ResolveMentionsByVisibility returns the users mentioned in an issue, removing those that +// don't have access to reading it. Teams are expanded into their users, but organizations are ignored. +func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, mentions []string) (users []*User, err error) { + if len(mentions) == 0 { + return + } + if err = issue.loadRepo(ctx.e); err != nil { + return + } + resolved := make(map[string]bool, 20) + names := make([]string, 0, 20) + resolved[doer.LowerName] = true + for _, name := range mentions { + name := strings.ToLower(name) + if _, ok := resolved[name]; ok { + continue + } + resolved[name] = false + names = append(names, name) + } + + if err := issue.Repo.getOwner(ctx.e); err != nil { + return nil, err + } + + if issue.Repo.Owner.IsOrganization() { + // Since there can be users with names that match the name of a team, + // if the team exists and can read the issue, the team takes precedence. + teams := make([]*Team, 0, len(names)) + if err := ctx.e. + Join("INNER", "team_repo", "team_repo.team_id = team.id"). + Where("team_repo.repo_id=?", issue.Repo.ID). + In("team.lower_name", names). + Find(&teams); err != nil { + return nil, fmt.Errorf("find mentioned teams: %v", err) + } + if len(teams) != 0 { + checked := make([]int64, 0, len(teams)) + unittype := UnitTypeIssues + if issue.IsPull { + unittype = UnitTypePullRequests + } + for _, team := range teams { + if team.Authorize >= AccessModeOwner { + checked = append(checked, team.ID) + resolved[team.LowerName] = true + continue + } + has, err := ctx.e.Get(&TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype}) + if err != nil { + return nil, fmt.Errorf("get team units (%d): %v", team.ID, err) + } + if has { + checked = append(checked, team.ID) + resolved[team.LowerName] = true + } + } + if len(checked) != 0 { + teamusers := make([]*User, 0, 20) + if err := ctx.e. + Join("INNER", "team_user", "team_user.uid = `user`.id"). + In("`team_user`.team_id", checked). + And("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + Find(&teamusers); err != nil { + return nil, fmt.Errorf("get teams users: %v", err) + } + if len(teamusers) > 0 { + users = make([]*User, 0, len(teamusers)) + for _, user := range teamusers { + if already, ok := resolved[user.LowerName]; !ok || !already { + users = append(users, user) + resolved[user.LowerName] = true + } + } + } + } + } + + // Remove names already in the list to avoid querying the database if pending names remain + names = make([]string, 0, len(resolved)) + for name, already := range resolved { + if !already { + names = append(names, name) + } + } + if len(names) == 0 { + return + } + } + + unchecked := make([]*User, 0, len(names)) + if err := ctx.e. + Where("`user`.is_active = ?", true). + And("`user`.prohibit_login = ?", false). + In("`user`.lower_name", names). + Find(&unchecked); err != nil { + return nil, fmt.Errorf("find mentioned users: %v", err) + } + for _, user := range unchecked { + if already := resolved[user.LowerName]; already || user.IsOrganization() { + continue + } + // Normal users must have read access to the referencing issue + perm, err := getUserRepoPermission(ctx.e, issue.Repo, user) + if err != nil { + return nil, fmt.Errorf("getUserRepoPermission [%d]: %v", user.ID, err) + } + if !perm.CanReadIssuesOrPulls(issue.IsPull) { + continue + } + users = append(users, user) + } + + return +} + +// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID +func UpdateIssuesMigrationsByType(gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error { + _, err := x.Table("issue"). + Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). + And("original_author_id = ?", originalAuthorID). + Update(map[string]interface{}{ + "poster_id": posterID, + "original_author": "", + "original_author_id": 0, + }) + return err +} diff --git a/models/issue_assignees.go b/models/issue_assignees.go index 1f504a9950..ed0576b38b 100644 --- a/models/issue_assignees.go +++ b/models/issue_assignees.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // IssueAssignees saves all issue assignees @@ -58,8 +58,11 @@ func getAssigneesByIssue(e Engine, issue *Issue) (assignees []*User, err error) // IsUserAssignedToIssue returns true when the user is assigned to the issue func IsUserAssignedToIssue(issue *Issue, user *User) (isAssigned bool, err error) { - isAssigned, err = x.Exist(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID}) - return + return isUserAssignedToIssue(x, issue, user) +} + +func isUserAssignedToIssue(e Engine, issue *Issue, user *User) (isAssigned bool, err error) { + return e.Get(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID}) } // DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array @@ -78,7 +81,7 @@ func DeleteNotPassedAssignee(issue *Issue, doer *User, assignees []*User) (err e if !found { // This function also does comments and hooks, which is why we call it seperatly instead of directly removing the assignees here - if err := UpdateAssignee(issue, doer, assignee.ID); err != nil { + if _, _, err := issue.ToggleAssignee(doer, assignee.ID); err != nil { return err } } @@ -110,73 +113,56 @@ func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) { return } -// AddAssigneeIfNotAssigned adds an assignee only if he isn't aleady assigned to the issue -func AddAssigneeIfNotAssigned(issue *Issue, doer *User, assigneeID int64) (err error) { - // Check if the user is already assigned - isAssigned, err := IsUserAssignedToIssue(issue, &User{ID: assigneeID}) - if err != nil { - return err - } - - if !isAssigned { - return issue.ChangeAssignee(doer, assigneeID) - } - return nil -} - -// UpdateAssignee deletes or adds an assignee to an issue -func UpdateAssignee(issue *Issue, doer *User, assigneeID int64) (err error) { - return issue.ChangeAssignee(doer, assigneeID) -} - -// ChangeAssignee changes the Assignee of this issue. -func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) { +// ToggleAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it. +func (issue *Issue) ToggleAssignee(doer *User, assigneeID int64) (removed bool, comment *Comment, err error) { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { - return err + return false, nil, err } - if err := issue.changeAssignee(sess, doer, assigneeID, false); err != nil { - return err + removed, comment, err = issue.toggleAssignee(sess, doer, assigneeID, false) + if err != nil { + return false, nil, err } if err := sess.Commit(); err != nil { - return err + return false, nil, err } go HookQueue.Add(issue.RepoID) - return nil + + return removed, comment, nil } -func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (err error) { - // Update the assignee - removed, err := updateIssueAssignee(sess, issue, assigneeID) +func (issue *Issue) toggleAssignee(sess *xorm.Session, doer *User, assigneeID int64, isCreate bool) (removed bool, comment *Comment, err error) { + removed, err = toggleUserAssignee(sess, issue, assigneeID) if err != nil { - return fmt.Errorf("UpdateIssueUserByAssignee: %v", err) + return false, nil, fmt.Errorf("UpdateIssueUserByAssignee: %v", err) } // Repo infos if err = issue.loadRepo(sess); err != nil { - return fmt.Errorf("loadRepo: %v", err) + return false, nil, fmt.Errorf("loadRepo: %v", err) } // Comment - if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil { - return fmt.Errorf("createAssigneeComment: %v", err) + comment, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed) + if err != nil { + return false, nil, fmt.Errorf("createAssigneeComment: %v", err) } // if pull request is in the middle of creation - don't call webhook if isCreate { - return nil + return removed, comment, err } if issue.IsPull { mode, _ := accessLevelUnit(sess, doer, issue.Repo, UnitTypePullRequests) if err = issue.loadPullRequest(sess); err != nil { - return fmt.Errorf("loadPullRequest: %v", err) + return false, nil, fmt.Errorf("loadPullRequest: %v", err) } issue.PullRequest.Issue = issue apiPullRequest := &api.PullRequestPayload{ @@ -190,9 +176,10 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in } else { apiPullRequest.Action = api.HookIssueAssigned } + // Assignee comment triggers a webhook if err := prepareWebhooks(sess, issue.Repo, HookEventPullRequest, apiPullRequest); err != nil { log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err) - return nil + return false, nil, err } } else { mode, _ := accessLevelUnit(sess, doer, issue.Repo, UnitTypeIssues) @@ -208,67 +195,50 @@ func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID in } else { apiIssue.Action = api.HookIssueAssigned } + // Assignee comment triggers a webhook if err := prepareWebhooks(sess, issue.Repo, HookEventIssues, apiIssue); err != nil { log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err) - return nil + return false, nil, err } } - return nil + return removed, comment, nil } -// UpdateAPIAssignee is a helper function to add or delete one or multiple issue assignee(s) -// Deleting is done the GitHub way (quote from their api documentation): -// https://developer.github.com/v3/issues/#edit-an-issue -// "assignees" (array): Logins for Users to assign to this issue. -// Pass one or more user logins to replace the set of assignees on this Issue. -// Send an empty array ([]) to clear all assignees from the Issue. -func UpdateAPIAssignee(issue *Issue, oneAssignee string, multipleAssignees []string, doer *User) (err error) { - var allNewAssignees []*User +// toggles user assignee state in database +func toggleUserAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) { - // Keep the old assignee thingy for compatibility reasons - if oneAssignee != "" { - // Prevent double adding assignees - var isDouble bool - for _, assignee := range multipleAssignees { - if assignee == oneAssignee { - isDouble = true - break - } - } + // Check if the user exists + assignee, err := getUserByID(e, assigneeID) + if err != nil { + return false, err + } - if !isDouble { - multipleAssignees = append(multipleAssignees, oneAssignee) + // Check if the submitted user is already assigned, if yes delete him otherwise add him + var i int + for i = 0; i < len(issue.Assignees); i++ { + if issue.Assignees[i].ID == assigneeID { + break } } - // Loop through all assignees to add them - for _, assigneeName := range multipleAssignees { - assignee, err := GetUserByName(assigneeName) + assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID} + + toBeDeleted := i < len(issue.Assignees) + if toBeDeleted { + issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i:]...) + _, err = e.Delete(assigneeIn) if err != nil { - return err + return toBeDeleted, err } - - allNewAssignees = append(allNewAssignees, assignee) - } - - // Delete all old assignees not passed - if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil { - return err - } - - // Add all new assignees - // Update the assignee. The function will check if the user exists, is already - // assigned (which he shouldn't as we deleted all assignees before) and - // has access to the repo. - for _, assignee := range allNewAssignees { - // Extra method to prevent double adding (which would result in removing) - err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID) + } else { + issue.Assignees = append(issue.Assignees, assignee) + _, err = e.Insert(assigneeIn) if err != nil { - return err + return toBeDeleted, err } } - return + return toBeDeleted, nil } // MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs @@ -292,7 +262,7 @@ func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string } // Get the IDs of all assignees - assigneeIDs = GetUserIDsByNames(multipleAssignees) + assigneeIDs, err = GetUserIDsByNames(multipleAssignees, false) return } diff --git a/models/issue_assignees_test.go b/models/issue_assignees_test.go index d32f41737a..1c5b5e7a22 100644 --- a/models/issue_assignees_test.go +++ b/models/issue_assignees_test.go @@ -20,17 +20,17 @@ func TestUpdateAssignee(t *testing.T) { // Assign multiple users user2, err := GetUserByID(2) assert.NoError(t, err) - err = UpdateAssignee(issue, &User{ID: 1}, user2.ID) + _, _, err = issue.ToggleAssignee(&User{ID: 1}, user2.ID) assert.NoError(t, err) user3, err := GetUserByID(3) assert.NoError(t, err) - err = UpdateAssignee(issue, &User{ID: 1}, user3.ID) + _, _, err = issue.ToggleAssignee(&User{ID: 1}, user3.ID) assert.NoError(t, err) user1, err := GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him assert.NoError(t, err) - err = UpdateAssignee(issue, &User{ID: 1}, user1.ID) + _, _, err = issue.ToggleAssignee(&User{ID: 1}, user1.ID) assert.NoError(t, err) // Check if he got removed diff --git a/models/issue_comment.go b/models/issue_comment.go index e8043c1ec7..d7128bdbac 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -13,12 +13,14 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/references" + "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "github.com/unknwon/com" "xorm.io/builder" + "xorm.io/xorm" ) // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. @@ -144,10 +146,10 @@ type Comment struct { // Reference an issue or pull from another comment, issue or PR // All information is about the origin of the reference - RefRepoID int64 `xorm:"index"` // Repo where the referencing - RefIssueID int64 `xorm:"index"` - RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's) - RefAction XRefAction `xorm:"SMALLINT"` // What hapens if RefIssueID resolves + RefRepoID int64 `xorm:"index"` // Repo where the referencing + RefIssueID int64 `xorm:"index"` + RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's) + RefAction references.XRefAction `xorm:"SMALLINT"` // What hapens if RefIssueID resolves RefIsPull bool RefRepo *Repository `xorm:"-"` @@ -355,6 +357,27 @@ func (c *Comment) LoadAttachments() error { return nil } +// UpdateAttachments update attachments by UUIDs for the comment +func (c *Comment) UpdateAttachments(uuids []string) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + attachments, err := getAttachmentsByUUIDs(sess, uuids) + if err != nil { + return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err) + } + for i := 0; i < len(attachments); i++ { + attachments[i].IssueID = c.IssueID + attachments[i].CommentID = c.ID + if err := updateAttachment(sess, attachments[i]); err != nil { + return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err) + } + } + return sess.Commit() +} + // LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees func (c *Comment) LoadAssigneeUser() error { var err error @@ -773,7 +796,7 @@ type CreateCommentOptions struct { RefRepoID int64 RefIssueID int64 RefCommentID int64 - RefAction XRefAction + RefAction references.XRefAction RefIsPull bool } @@ -1021,3 +1044,23 @@ func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) { return fetchCodeComments(x, issue, currentUser) } + +// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id +func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error { + _, err := x.Table("comment"). + Where(builder.In("issue_id", + builder.Select("issue.id"). + From("issue"). + InnerJoin("repository", "issue.repo_id = repository.id"). + Where(builder.Eq{ + "repository.original_service_type": tp, + }), + )). + And("comment.original_author_id = ?", originalAuthorID). + Update(map[string]interface{}{ + "poster_id": posterID, + "original_author": "", + "original_author_id": 0, + }) + return err +} diff --git a/models/issue_label.go b/models/issue_label.go index dab5ba2827..1fc873cfd4 100644 --- a/models/issue_label.go +++ b/models/issue_label.go @@ -13,8 +13,8 @@ import ( api "code.gitea.io/gitea/modules/structs" - "github.com/go-xorm/xorm" "xorm.io/builder" + "xorm.io/xorm" ) var labelColorPattern = regexp.MustCompile("#([a-fA-F0-9]{6})") @@ -68,10 +68,11 @@ type Label struct { Color string `xorm:"VARCHAR(7)"` NumIssues int NumClosedIssues int - NumOpenIssues int `xorm:"-"` - IsChecked bool `xorm:"-"` - QueryString string - IsSelected bool + NumOpenIssues int `xorm:"-"` + IsChecked bool `xorm:"-"` + QueryString string `xorm:"-"` + IsSelected bool `xorm:"-"` + IsExcluded bool `xorm:"-"` } // APIFormat converts a Label to the api.Label format @@ -97,7 +98,10 @@ func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) for _, s := range currentSelectedLabels { if s == label.ID { labelSelected = true - } else if s > 0 { + } else if -s == label.ID { + labelSelected = true + label.IsExcluded = true + } else if s != 0 { labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10)) } } diff --git a/models/issue_lock.go b/models/issue_lock.go index 5a2d996b64..dc6655ad3b 100644 --- a/models/issue_lock.go +++ b/models/issue_lock.go @@ -28,7 +28,6 @@ func updateIssueLock(opts *IssueLockOptions, lock bool) error { } opts.Issue.IsLocked = lock - var commentType CommentType if opts.Issue.IsLocked { commentType = CommentTypeLock @@ -36,16 +35,26 @@ func updateIssueLock(opts *IssueLockOptions, lock bool) error { commentType = CommentTypeUnlock } - if err := UpdateIssueCols(opts.Issue, "is_locked"); err != nil { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { return err } - _, err := CreateComment(&CreateCommentOptions{ + if err := updateIssueCols(sess, opts.Issue, "is_locked"); err != nil { + return err + } + + _, err := createComment(sess, &CreateCommentOptions{ Doer: opts.Doer, Issue: opts.Issue, Repo: opts.Issue.Repo, Type: commentType, Content: opts.Reason, }) - return err + if err != nil { + return err + } + + return sess.Commit() } diff --git a/models/issue_milestone.go b/models/issue_milestone.go index f8f414e716..d32cb3c7d1 100644 --- a/models/issue_milestone.go +++ b/models/issue_milestone.go @@ -10,8 +10,9 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "xorm.io/builder" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // Milestone represents a milestone of repository. @@ -191,7 +192,6 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 { // GetMilestonesByRepoID returns all opened milestones of a repository. func GetMilestonesByRepoID(repoID int64, state api.StateType) (MilestoneList, error) { - sess := x.Where("repo_id = ?", repoID) switch state { @@ -238,13 +238,34 @@ func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (Mile } func updateMilestone(e Engine, m *Milestone) error { - _, err := e.ID(m.ID).AllCols().Update(m) + _, err := e.ID(m.ID).AllCols(). + SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( + builder.Eq{"milestone_id": m.ID}, + )). + SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where( + builder.Eq{ + "milestone_id": m.ID, + "is_closed": true, + }, + )). + Update(m) return err } // UpdateMilestone updates information of given milestone. func UpdateMilestone(m *Milestone) error { - return updateMilestone(x, m) + if err := updateMilestone(x, m); err != nil { + return err + } + + return updateMilestoneCompleteness(x, m.ID) +} + +func updateMilestoneCompleteness(e Engine, milestoneID int64) error { + _, err := e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?", + milestoneID, + ) + return err } func countRepoMilestones(e Engine, repoID int64) (int64, error) { @@ -278,11 +299,6 @@ func MilestoneStats(repoID int64) (open int64, closed int64, err error) { // ChangeMilestoneStatus changes the milestone open/closed status. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { - repo, err := GetRepositoryByID(m.RepoID) - if err != nil { - return err - } - sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -290,92 +306,88 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { } m.IsClosed = isClosed - if err = updateMilestone(sess, m); err != nil { + if _, err := sess.ID(m.ID).Cols("is_closed").Update(m); err != nil { return err } - numMilestones, err := countRepoMilestones(sess, repo.ID) - if err != nil { + if err := updateRepoMilestoneNum(sess, m.RepoID); err != nil { return err } - numClosedMilestones, err := countRepoClosedMilestones(sess, repo.ID) - if err != nil { - return err - } - repo.NumMilestones = int(numMilestones) - repo.NumClosedMilestones = int(numClosedMilestones) - if _, err = sess.ID(repo.ID).Cols("num_milestones, num_closed_milestones").Update(repo); err != nil { - return err - } return sess.Commit() } -func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error { - if issue.MilestoneID == 0 { - return nil +func updateRepoMilestoneNum(e Engine, repoID int64) error { + _, err := e.Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?", + repoID, + repoID, + true, + repoID, + ) + return err +} + +func updateMilestoneTotalNum(e Engine, milestoneID int64) (err error) { + if _, err = e.Exec("UPDATE `milestone` SET num_issues=(SELECT count(*) FROM issue WHERE milestone_id=?) WHERE id=?", + milestoneID, + milestoneID, + ); err != nil { + return } - m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID) - if err != nil { - return err + return updateMilestoneCompleteness(e, milestoneID) +} + +func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) { + if _, err = e.Exec("UPDATE `milestone` SET num_closed_issues=(SELECT count(*) FROM issue WHERE milestone_id=? AND is_closed=?) WHERE id=?", + milestoneID, + true, + milestoneID, + ); err != nil { + return } - if issue.IsClosed { - m.NumOpenIssues-- - m.NumClosedIssues++ - } else { - m.NumOpenIssues++ - m.NumClosedIssues-- - } - - return updateMilestone(e, m) + return updateMilestoneCompleteness(e, milestoneID) } func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error { + if err := updateIssueCols(e, issue, "milestone_id"); err != nil { + return err + } + if oldMilestoneID > 0 { - m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID) - if err != nil { + if err := updateMilestoneTotalNum(e, oldMilestoneID); err != nil { return err } - - m.NumIssues-- if issue.IsClosed { - m.NumClosedIssues-- - } - - if err = updateMilestone(e, m); err != nil { - return err + if err := updateMilestoneClosedNum(e, oldMilestoneID); err != nil { + return err + } } } if issue.MilestoneID > 0 { - m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID) - if err != nil { + if err := updateMilestoneTotalNum(e, issue.MilestoneID); err != nil { return err } - - m.NumIssues++ if issue.IsClosed { - m.NumClosedIssues++ + if err := updateMilestoneClosedNum(e, issue.MilestoneID); err != nil { + return err + } } - - if err = updateMilestone(e, m); err != nil { - return err - } - } - - if err := issue.loadRepo(e); err != nil { - return err } if oldMilestoneID > 0 || issue.MilestoneID > 0 { + if err := issue.loadRepo(e); err != nil { + return err + } + if _, err := createMilestoneComment(e, doer, issue.Repo, issue, oldMilestoneID, issue.MilestoneID); err != nil { return err } } - return updateIssueCols(e, issue, "milestone_id") + return nil } // ChangeMilestoneAssign changes assignment of milestone for issue. diff --git a/models/issue_milestone_test.go b/models/issue_milestone_test.go index 09c6ff7595..6f8548ec67 100644 --- a/models/issue_milestone_test.go +++ b/models/issue_milestone_test.go @@ -231,7 +231,7 @@ func TestChangeMilestoneStatus(t *testing.T) { CheckConsistencyFor(t, &Repository{ID: milestone.RepoID}, &Milestone{}) } -func TestChangeMilestoneIssueStats(t *testing.T) { +func TestUpdateMilestoneClosedNum(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) issue := AssertExistsAndLoadBean(t, &Issue{MilestoneID: 1}, "is_closed=0").(*Issue) @@ -240,14 +240,14 @@ func TestChangeMilestoneIssueStats(t *testing.T) { issue.ClosedUnix = timeutil.TimeStampNow() _, err := x.Cols("is_closed", "closed_unix").Update(issue) assert.NoError(t, err) - assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue)) + assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID)) CheckConsistencyFor(t, &Milestone{}) issue.IsClosed = false issue.ClosedUnix = 0 _, err = x.Cols("is_closed", "closed_unix").Update(issue) assert.NoError(t, err) - assert.NoError(t, changeMilestoneIssueStats(x.NewSession(), issue)) + assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID)) CheckConsistencyFor(t, &Milestone{}) } diff --git a/models/issue_reaction.go b/models/issue_reaction.go index ab644b4b3e..4596d32d06 100644 --- a/models/issue_reaction.go +++ b/models/issue_reaction.go @@ -11,8 +11,8 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "xorm.io/builder" + "xorm.io/xorm" ) // Reaction represents a reactions on issues and comments. diff --git a/models/issue_test.go b/models/issue_test.go index 9cd9ff0ad9..592a0e3d77 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -84,53 +84,6 @@ func TestGetParticipantsByIssueID(t *testing.T) { checkParticipants(1, []int{5}) } -func TestIssue_AddLabel(t *testing.T) { - var tests = []struct { - issueID int64 - labelID int64 - doerID int64 - }{ - {1, 2, 2}, // non-pull-request, not-already-added label - {1, 1, 2}, // non-pull-request, already-added label - {2, 2, 2}, // pull-request, not-already-added label - {2, 1, 2}, // pull-request, already-added label - } - for _, test := range tests { - assert.NoError(t, PrepareTestDatabase()) - issue := AssertExistsAndLoadBean(t, &Issue{ID: test.issueID}).(*Issue) - label := AssertExistsAndLoadBean(t, &Label{ID: test.labelID}).(*Label) - doer := AssertExistsAndLoadBean(t, &User{ID: test.doerID}).(*User) - assert.NoError(t, issue.AddLabel(doer, label)) - AssertExistsAndLoadBean(t, &IssueLabel{IssueID: test.issueID, LabelID: test.labelID}) - } -} - -func TestIssue_AddLabels(t *testing.T) { - var tests = []struct { - issueID int64 - labelIDs []int64 - doerID int64 - }{ - {1, []int64{1, 2}, 2}, // non-pull-request - {1, []int64{}, 2}, // non-pull-request, empty - {2, []int64{1, 2}, 2}, // pull-request - {2, []int64{}, 1}, // pull-request, empty - } - for _, test := range tests { - assert.NoError(t, PrepareTestDatabase()) - issue := AssertExistsAndLoadBean(t, &Issue{ID: test.issueID}).(*Issue) - labels := make([]*Label, len(test.labelIDs)) - for i, labelID := range test.labelIDs { - labels[i] = AssertExistsAndLoadBean(t, &Label{ID: labelID}).(*Label) - } - doer := AssertExistsAndLoadBean(t, &User{ID: test.doerID}).(*User) - assert.NoError(t, issue.AddLabels(doer, labels)) - for _, labelID := range test.labelIDs { - AssertExistsAndLoadBean(t, &IssueLabel{IssueID: test.issueID, LabelID: labelID}) - } - } -} - func TestIssue_ClearLabels(t *testing.T) { var tests = []struct { issueID int64 @@ -160,7 +113,7 @@ func TestUpdateIssueCols(t *testing.T) { issue.Content = "This should have no effect" now := time.Now().Unix() - assert.NoError(t, UpdateIssueCols(issue, "name")) + assert.NoError(t, updateIssueCols(x, issue, "name")) then := time.Now().Unix() updatedIssue := AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue) @@ -344,7 +297,7 @@ func testInsertIssue(t *testing.T, title, content string) { Title: title, Content: content, } - err := NewIssue(repo, &issue, nil, nil, nil) + err := NewIssue(repo, &issue, nil, nil) assert.NoError(t, err) var newIssue Issue @@ -366,3 +319,35 @@ func TestIssue_InsertIssue(t *testing.T) { testInsertIssue(t, "my issue1", "special issue's comments?") testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?") } + +func TestIssue_ResolveMentions(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + testSuccess := func(owner, repo, doer string, mentions []string, expected []int64) { + o := AssertExistsAndLoadBean(t, &User{LowerName: owner}).(*User) + r := AssertExistsAndLoadBean(t, &Repository{OwnerID: o.ID, LowerName: repo}).(*Repository) + issue := &Issue{RepoID: r.ID} + d := AssertExistsAndLoadBean(t, &User{LowerName: doer}).(*User) + resolved, err := issue.ResolveMentionsByVisibility(DefaultDBContext(), d, mentions) + assert.NoError(t, err) + ids := make([]int64, len(resolved)) + for i, user := range resolved { + ids[i] = user.ID + } + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + assert.EqualValues(t, expected, ids) + } + + // Public repo, existing user + testSuccess("user2", "repo1", "user1", []string{"user5"}, []int64{5}) + // Public repo, non-existing user + testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{}) + // Public repo, doer + testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{}) + // Private repo, team member + testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2}) + // Private repo, not a team member + testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{}) + // Private repo, whole team + testSuccess("user17", "big_test_private_4", "user15", []string{"owners"}, []int64{18}) +} diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go index f9313b7653..f616836c85 100644 --- a/models/issue_tracked_time.go +++ b/models/issue_tracked_time.go @@ -10,8 +10,8 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "github.com/go-xorm/xorm" "xorm.io/builder" + "xorm.io/xorm" ) // TrackedTime represents a time that was spent for a specific issue. diff --git a/models/issue_user.go b/models/issue_user.go index d55a0dc2fb..67a118fe57 100644 --- a/models/issue_user.go +++ b/models/issue_user.go @@ -6,8 +6,6 @@ package models import ( "fmt" - - "github.com/go-xorm/xorm" ) // IssueUser represents an issue-user relation. @@ -51,42 +49,6 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error { return nil } -func updateIssueAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) { - - // Check if the user exists - assignee, err := getUserByID(e, assigneeID) - if err != nil { - return false, err - } - - // Check if the submitted user is already assigne, if yes delete him otherwise add him - var i int - for i = 0; i < len(issue.Assignees); i++ { - if issue.Assignees[i].ID == assigneeID { - break - } - } - - assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID} - - toBeDeleted := i < len(issue.Assignees) - if toBeDeleted { - issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i:]...) - _, err = e.Delete(assigneeIn) - if err != nil { - return toBeDeleted, err - } - } else { - issue.Assignees = append(issue.Assignees, assignee) - _, err = e.Insert(assigneeIn) - if err != nil { - return toBeDeleted, err - } - } - - return toBeDeleted, nil -} - // UpdateIssueUserByRead updates issue-user relation for reading. func UpdateIssueUserByRead(uid, issueID int64) error { _, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID) diff --git a/models/issue_xref.go b/models/issue_xref.go index 1cc0bcfe6a..4b01022bc5 100644 --- a/models/issue_xref.go +++ b/models/issue_xref.go @@ -5,42 +5,16 @@ package models import ( - "regexp" - "strconv" - "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/references" - "github.com/go-xorm/xorm" "github.com/unknwon/com" -) - -var ( - // TODO: Unify all regexp treatment of cross references in one place - - // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 - issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(?:#)([0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`) - // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository - // e.g. gogits/gogs#12345 - crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+)#([0-9]+)(?:\s|$|\)|\]|\.(\s|$))`) -) - -// XRefAction represents the kind of effect a cross reference has once is resolved -type XRefAction int64 - -const ( - // XRefActionNone means the cross-reference is a mention (commit, etc.) - XRefActionNone XRefAction = iota // 0 - // XRefActionCloses means the cross-reference should close an issue if it is resolved - XRefActionCloses // 1 - not implemented yet - // XRefActionReopens means the cross-reference should reopen an issue if it is resolved - XRefActionReopens // 2 - Not implemented yet - // XRefActionNeutered means the cross-reference will no longer affect the source - XRefActionNeutered // 3 + "xorm.io/xorm" ) type crossReference struct { Issue *Issue - Action XRefAction + Action references.XRefAction } // crossReferencesContext is context to pass along findCrossReference functions @@ -72,7 +46,7 @@ func newCrossReference(e *xorm.Session, ctx *crossReferencesContext, xref *cross func neuterCrossReferences(e Engine, issueID int64, commentID int64) error { active := make([]*Comment, 0, 10) - sess := e.Where("`ref_action` IN (?, ?, ?)", XRefActionNone, XRefActionCloses, XRefActionReopens) + sess := e.Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens) if issueID != 0 { sess = sess.And("`ref_issue_id` = ?", issueID) } @@ -86,7 +60,7 @@ func neuterCrossReferences(e Engine, issueID int64, commentID int64) error { for i, c := range active { ids[i] = c.ID } - _, err := e.In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: XRefActionNeutered}) + _, err := e.In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered}) return err } @@ -110,11 +84,11 @@ func (issue *Issue) addCrossReferences(e *xorm.Session, doer *User) error { Doer: doer, OrigIssue: issue, } - return issue.createCrossReferences(e, ctx, issue.Title+"\n"+issue.Content) + return issue.createCrossReferences(e, ctx, issue.Title, issue.Content) } -func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesContext, content string) error { - xreflist, err := ctx.OrigIssue.getCrossReferences(e, ctx, content) +func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) error { + xreflist, err := ctx.OrigIssue.getCrossReferences(e, ctx, plaincontent, mdcontent) if err != nil { return err } @@ -126,47 +100,43 @@ func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesC return nil } -func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesContext, content string) ([]*crossReference, error) { +func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) { xreflist := make([]*crossReference, 0, 5) - var xref *crossReference + var ( + refRepo *Repository + refIssue *Issue + err error + ) - // Issues in the same repository - // FIXME: Should we support IssueNameStyleAlphanumeric? - matches := issueNumericPattern.FindAllStringSubmatch(content, -1) - for _, match := range matches { - if index, err := strconv.ParseInt(match[1], 10, 64); err == nil { - if err = ctx.OrigIssue.loadRepo(e); err != nil { + allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...) + + for _, ref := range allrefs { + if ref.Owner == "" && ref.Name == "" { + // Issues in the same repository + if err := ctx.OrigIssue.loadRepo(e); err != nil { return nil, err } - if xref, err = ctx.OrigIssue.isValidCommentReference(e, ctx, issue.Repo, index); err != nil { - return nil, err - } - if xref != nil { - xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, xref) - } - } - } - - // Issues in other repositories - matches = crossReferenceIssueNumericPattern.FindAllStringSubmatch(content, -1) - for _, match := range matches { - if index, err := strconv.ParseInt(match[3], 10, 64); err == nil { - repo, err := getRepositoryByOwnerAndName(e, match[1], match[2]) + refRepo = ctx.OrigIssue.Repo + } else { + // Issues in other repositories + refRepo, err = getRepositoryByOwnerAndName(e, ref.Owner, ref.Name) if err != nil { if IsErrRepoNotExist(err) { continue } return nil, err } - if err = ctx.OrigIssue.loadRepo(e); err != nil { - return nil, err - } - if xref, err = issue.isValidCommentReference(e, ctx, repo, index); err != nil { - return nil, err - } - if xref != nil { - xreflist = issue.updateCrossReferenceList(xreflist, xref) - } + } + if refIssue, err = ctx.OrigIssue.findReferencedIssue(e, ctx, refRepo, ref.Index); err != nil { + return nil, err + } + if refIssue != nil { + xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{ + Issue: refIssue, + // FIXME: currently ignore keywords + // Action: ref.Action, + Action: references.XRefActionNone, + }) } } @@ -179,7 +149,7 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross } for i, r := range list { if r.Issue.ID == xref.Issue.ID { - if xref.Action != XRefActionNone { + if xref.Action != references.XRefActionNone { list[i].Action = xref.Action } return list @@ -188,7 +158,7 @@ func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *cross return append(list, xref) } -func (issue *Issue) isValidCommentReference(e Engine, ctx *crossReferencesContext, repo *Repository, index int64) (*crossReference, error) { +func (issue *Issue) findReferencedIssue(e Engine, ctx *crossReferencesContext, repo *Repository, index int64) (*Issue, error) { refIssue := &Issue{RepoID: repo.ID, Index: index} if has, _ := e.Get(refIssue); !has { return nil, nil @@ -206,10 +176,7 @@ func (issue *Issue) isValidCommentReference(e Engine, ctx *crossReferencesContex return nil, nil } } - return &crossReference{ - Issue: refIssue, - Action: XRefActionNone, - }, nil + return refIssue, nil } func (issue *Issue) neuterCrossReferences(e Engine) error { @@ -237,7 +204,7 @@ func (comment *Comment) addCrossReferences(e *xorm.Session, doer *User) error { OrigIssue: comment.Issue, OrigComment: comment, } - return comment.Issue.createCrossReferences(e, ctx, comment.Content) + return comment.Issue.createCrossReferences(e, ctx, "", comment.Content) } func (comment *Comment) neuterCrossReferences(e Engine) error { diff --git a/models/lfs_lock.go b/models/lfs_lock.go index 7ea1dc8660..ba1a452815 100644 --- a/models/lfs_lock.go +++ b/models/lfs_lock.go @@ -14,7 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // LFSLock represents a git lfs lock of repository. diff --git a/models/login_source.go b/models/login_source.go index 9381ed034f..ce03c4154f 100644 --- a/models/login_source.go +++ b/models/login_source.go @@ -21,9 +21,9 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "github.com/unknwon/com" "xorm.io/core" + "xorm.io/xorm" ) // LoginType represents an login type. diff --git a/models/migrate.go b/models/migrate.go index 85be3a312c..53838fd65e 100644 --- a/models/migrate.go +++ b/models/migrate.go @@ -4,7 +4,7 @@ package models -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" // InsertMilestones creates milestones of repository. func InsertMilestones(ms ...*Milestone) (err error) { diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 89350ee89d..5ed70dc4f5 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -21,10 +21,10 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" gouuid "github.com/satori/go.uuid" "github.com/unknwon/com" ini "gopkg.in/ini.v1" + "xorm.io/xorm" ) const minDBVersion = 4 @@ -253,6 +253,18 @@ var migrations = []Migration{ // v98 -> v99 NewMigration("add original author name and id on migrated release", addOriginalAuthorOnMigratedReleases), // v99 -> v100 + NewMigration("add task table and status column for repository table", addTaskTable), + // v100 -> v101 + NewMigration("update migration repositories' service type", updateMigrationServiceTypes), + // v101 -> v102 + NewMigration("change length of some external login users columns", changeSomeColumnsLengthOfExternalLoginUser), + // v102 -> v103 + NewMigration("update migration repositories' service type", dropColumnHeadUserNameOnPullRequest), + // v103 -> v104 + NewMigration("Add WhitelistDeployKeys to protected branch", addWhitelistDeployKeysToBranches), + // v104 -> v105 + NewMigration("remove unnecessary columns from label", removeLabelUneededCols), + // v105 -> v106 NewMigration("add includes_all_repositories to teams", addTeamIncludesAllRepositories), } @@ -406,9 +418,11 @@ func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin } for _, index := range res { indexName := index["column_name"] - _, err := sess.Exec(fmt.Sprintf("DROP INDEX `%s` ON `%s`", indexName, tableName)) - if err != nil { - return err + if len(indexName) > 0 { + _, err := sess.Exec(fmt.Sprintf("DROP INDEX `%s` ON `%s`", indexName, tableName)) + if err != nil { + return err + } } } diff --git a/models/migrations/v100.go b/models/migrations/v100.go new file mode 100644 index 0000000000..6a4e98af1f --- /dev/null +++ b/models/migrations/v100.go @@ -0,0 +1,83 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "net/url" + "strings" + "time" + + "xorm.io/xorm" +) + +func updateMigrationServiceTypes(x *xorm.Engine) error { + type Repository struct { + ID int64 + OriginalServiceType int `xorm:"index default(0)"` + OriginalURL string `xorm:"VARCHAR(2048)"` + } + + if err := x.Sync2(new(Repository)); err != nil { + return err + } + + var last int + const batchSize = 50 + for { + var results = make([]Repository, 0, batchSize) + err := x.Where("original_url <> '' AND original_url IS NOT NULL"). + And("original_service_type = 0 OR original_service_type IS NULL"). + OrderBy("id"). + Limit(batchSize, last). + Find(&results) + if err != nil { + return err + } + if len(results) == 0 { + break + } + last += len(results) + + const PlainGitService = 1 // 1 plain git service + const GithubService = 2 // 2 github.com + + for _, res := range results { + u, err := url.Parse(res.OriginalURL) + if err != nil { + return err + } + var serviceType = PlainGitService + if strings.EqualFold(u.Host, "github.com") { + serviceType = GithubService + } + _, err = x.Exec("UPDATE repository SET original_service_type = ? WHERE id = ?", serviceType, res.ID) + if err != nil { + return err + } + } + } + + type ExternalLoginUser struct { + ExternalID string `xorm:"pk NOT NULL"` + UserID int64 `xorm:"INDEX NOT NULL"` + LoginSourceID int64 `xorm:"pk NOT NULL"` + RawData map[string]interface{} `xorm:"TEXT JSON"` + Provider string `xorm:"index VARCHAR(25)"` + Email string + Name string + FirstName string + LastName string + NickName string + Description string + AvatarURL string + Location string + AccessToken string + AccessTokenSecret string + RefreshToken string + ExpiresAt time.Time + } + + return x.Sync2(new(ExternalLoginUser)) +} diff --git a/models/migrations/v101.go b/models/migrations/v101.go new file mode 100644 index 0000000000..9ef82a2933 --- /dev/null +++ b/models/migrations/v101.go @@ -0,0 +1,19 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func changeSomeColumnsLengthOfExternalLoginUser(x *xorm.Engine) error { + type ExternalLoginUser struct { + AccessToken string `xorm:"TEXT"` + AccessTokenSecret string `xorm:"TEXT"` + RefreshToken string `xorm:"TEXT"` + } + + return x.Sync2(new(ExternalLoginUser)) +} diff --git a/models/migrations/v102.go b/models/migrations/v102.go new file mode 100644 index 0000000000..74e8574ec3 --- /dev/null +++ b/models/migrations/v102.go @@ -0,0 +1,15 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func dropColumnHeadUserNameOnPullRequest(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + return dropTableColumns(sess, "pull_request", "head_user_name") +} diff --git a/models/migrations/v103.go b/models/migrations/v103.go new file mode 100644 index 0000000000..fed025c5cd --- /dev/null +++ b/models/migrations/v103.go @@ -0,0 +1,18 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func addWhitelistDeployKeysToBranches(x *xorm.Engine) error { + type ProtectedBranch struct { + ID int64 + WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` + } + + return x.Sync2(new(ProtectedBranch)) +} diff --git a/models/migrations/v104.go b/models/migrations/v104.go new file mode 100644 index 0000000000..f3ec3c88c8 --- /dev/null +++ b/models/migrations/v104.go @@ -0,0 +1,34 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func removeLabelUneededCols(x *xorm.Engine) error { + + // Make sure the columns exist before dropping them + type Label struct { + QueryString string + IsSelected bool + } + if err := x.Sync2(new(Label)); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + if err := dropTableColumns(sess, "label", "query_string"); err != nil { + return err + } + if err := dropTableColumns(sess, "label", "is_selected"); err != nil { + return err + } + return sess.Commit() +} diff --git a/models/migrations/v105.go b/models/migrations/v105.go new file mode 100644 index 0000000000..6c9a5817af --- /dev/null +++ b/models/migrations/v105.go @@ -0,0 +1,25 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func addTeamIncludesAllRepositories(x *xorm.Engine) error { + + type Team struct { + ID int64 `xorm:"pk autoincr"` + IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` + } + + if err := x.Sync2(new(Team)); err != nil { + return err + } + + _, err := x.Exec("UPDATE `team` SET `includes_all_repositories` = ? WHERE `name`=?", + true, "Owners") + return err +} diff --git a/models/migrations/v13.go b/models/migrations/v13.go index 8b6b38cadf..3c35b66ab9 100644 --- a/models/migrations/v13.go +++ b/models/migrations/v13.go @@ -9,8 +9,8 @@ import ( "fmt" "strings" - "github.com/go-xorm/xorm" "github.com/unknwon/com" + "xorm.io/xorm" ) func ldapUseSSLToSecurityProtocol(x *xorm.Engine) error { diff --git a/models/migrations/v14.go b/models/migrations/v14.go index 392f9fdba6..675c7459dd 100644 --- a/models/migrations/v14.go +++ b/models/migrations/v14.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func setCommentUpdatedWithCreated(x *xorm.Engine) (err error) { diff --git a/models/migrations/v15.go b/models/migrations/v15.go index 3492a7190b..8872f1e946 100644 --- a/models/migrations/v15.go +++ b/models/migrations/v15.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func createAllowCreateOrganizationColumn(x *xorm.Engine) error { diff --git a/models/migrations/v16.go b/models/migrations/v16.go index 5b8ec19d32..a849205b55 100644 --- a/models/migrations/v16.go +++ b/models/migrations/v16.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/markup" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // Enumerate all the unit types diff --git a/models/migrations/v17.go b/models/migrations/v17.go index 2986badc97..2907b009db 100644 --- a/models/migrations/v17.go +++ b/models/migrations/v17.go @@ -8,7 +8,7 @@ import ( "fmt" "time" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func setProtectedBranchUpdatedWithCreated(x *xorm.Engine) (err error) { diff --git a/models/migrations/v18.go b/models/migrations/v18.go index 3b3cd23ccf..66a1de3499 100644 --- a/models/migrations/v18.go +++ b/models/migrations/v18.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // ExternalLoginUser makes the connecting between some existing user and additional external login sources diff --git a/models/migrations/v19.go b/models/migrations/v19.go index 7728f5add6..349d5850aa 100644 --- a/models/migrations/v19.go +++ b/models/migrations/v19.go @@ -13,8 +13,8 @@ import ( "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" "github.com/unknwon/com" + "xorm.io/xorm" ) func generateAndMigrateGitHooks(x *xorm.Engine) (err error) { diff --git a/models/migrations/v20.go b/models/migrations/v20.go index ded99e09ce..0897eada74 100644 --- a/models/migrations/v20.go +++ b/models/migrations/v20.go @@ -16,7 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func useNewNameAvatars(x *xorm.Engine) error { diff --git a/models/migrations/v21.go b/models/migrations/v21.go index 65cae2ac03..2750725760 100644 --- a/models/migrations/v21.go +++ b/models/migrations/v21.go @@ -11,8 +11,8 @@ import ( "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" "github.com/unknwon/com" + "xorm.io/xorm" ) const ( diff --git a/models/migrations/v22.go b/models/migrations/v22.go index faac74343b..eb37aec17f 100644 --- a/models/migrations/v22.go +++ b/models/migrations/v22.go @@ -13,8 +13,8 @@ import ( "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" "github.com/unknwon/com" + "xorm.io/xorm" ) func generateAndMigrateWikiGitHooks(x *xorm.Engine) (err error) { diff --git a/models/migrations/v23.go b/models/migrations/v23.go index 4aadf7ef0d..50dc6cd2c7 100644 --- a/models/migrations/v23.go +++ b/models/migrations/v23.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // UserOpenID is the list of all OpenID identities of a user. diff --git a/models/migrations/v24.go b/models/migrations/v24.go index 076c710cc3..20791d7981 100644 --- a/models/migrations/v24.go +++ b/models/migrations/v24.go @@ -7,7 +7,7 @@ package migrations import ( "time" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func changeGPGKeysColumns(x *xorm.Engine) error { diff --git a/models/migrations/v25.go b/models/migrations/v25.go index a8d746590a..da74e27c28 100644 --- a/models/migrations/v25.go +++ b/models/migrations/v25.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addUserOpenIDShow(x *xorm.Engine) error { diff --git a/models/migrations/v26.go b/models/migrations/v26.go index 04277191f5..03ce2ef94b 100644 --- a/models/migrations/v26.go +++ b/models/migrations/v26.go @@ -16,8 +16,8 @@ import ( "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" "github.com/unknwon/com" + "xorm.io/xorm" ) func generateAndMigrateGitHookChains(x *xorm.Engine) (err error) { diff --git a/models/migrations/v27.go b/models/migrations/v27.go index 12e5fbcdbf..2bba0b7412 100644 --- a/models/migrations/v27.go +++ b/models/migrations/v27.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func convertIntervalToDuration(x *xorm.Engine) (err error) { diff --git a/models/migrations/v28.go b/models/migrations/v28.go index a30cbf2afb..587e944ce6 100644 --- a/models/migrations/v28.go +++ b/models/migrations/v28.go @@ -14,7 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addRepoSize(x *xorm.Engine) (err error) { diff --git a/models/migrations/v29.go b/models/migrations/v29.go index eadb0f3d87..ea70a2dd77 100644 --- a/models/migrations/v29.go +++ b/models/migrations/v29.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // CommitStatus see models/status.go diff --git a/models/migrations/v30.go b/models/migrations/v30.go index 90047df8b6..5acdc5dac7 100644 --- a/models/migrations/v30.go +++ b/models/migrations/v30.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addExternalLoginUserPK(x *xorm.Engine) error { diff --git a/models/migrations/v31.go b/models/migrations/v31.go index d6cea4c51b..b3aef0d665 100644 --- a/models/migrations/v31.go +++ b/models/migrations/v31.go @@ -8,8 +8,8 @@ import ( "fmt" "time" - "github.com/go-xorm/xorm" "xorm.io/core" + "xorm.io/xorm" ) func addLoginSourceSyncEnabledColumn(x *xorm.Engine) error { diff --git a/models/migrations/v32.go b/models/migrations/v32.go index d209fc34f6..f5c021cccf 100644 --- a/models/migrations/v32.go +++ b/models/migrations/v32.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addUnitsToRepoTeam(x *xorm.Engine) error { type Team struct { diff --git a/models/migrations/v33.go b/models/migrations/v33.go index 566951db96..625c5f4a53 100644 --- a/models/migrations/v33.go +++ b/models/migrations/v33.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func removeActionColumns(x *xorm.Engine) error { diff --git a/models/migrations/v34.go b/models/migrations/v34.go index 258da41c04..26f0f565f7 100644 --- a/models/migrations/v34.go +++ b/models/migrations/v34.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // Team see models/team.go diff --git a/models/migrations/v35.go b/models/migrations/v35.go index 7746663a40..d5059c7998 100644 --- a/models/migrations/v35.go +++ b/models/migrations/v35.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addCommentIDToAction(x *xorm.Engine) error { diff --git a/models/migrations/v36.go b/models/migrations/v36.go index 06f76a26d6..729019925e 100644 --- a/models/migrations/v36.go +++ b/models/migrations/v36.go @@ -7,7 +7,7 @@ package migrations import ( "code.gitea.io/gitea/models" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func regenerateGitHooks36(x *xorm.Engine) (err error) { diff --git a/models/migrations/v37.go b/models/migrations/v37.go index 00653a780d..29e1c966f3 100644 --- a/models/migrations/v37.go +++ b/models/migrations/v37.go @@ -7,7 +7,7 @@ package migrations import ( "html" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func unescapeUserFullNames(x *xorm.Engine) (err error) { diff --git a/models/migrations/v38.go b/models/migrations/v38.go index 6060b70fe8..4e4e6628d3 100644 --- a/models/migrations/v38.go +++ b/models/migrations/v38.go @@ -9,8 +9,8 @@ import ( "code.gitea.io/gitea/models" - "github.com/go-xorm/xorm" "xorm.io/core" + "xorm.io/xorm" ) func removeCommitsUnitType(x *xorm.Engine) (err error) { diff --git a/models/migrations/v39.go b/models/migrations/v39.go index 1312cb3313..f3b32ea873 100644 --- a/models/migrations/v39.go +++ b/models/migrations/v39.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // ReleaseV39 describes the added field for Release diff --git a/models/migrations/v40.go b/models/migrations/v40.go index fffe158bf9..944377ce9b 100644 --- a/models/migrations/v40.go +++ b/models/migrations/v40.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func fixProtectedBranchCanPushValue(x *xorm.Engine) error { diff --git a/models/migrations/v41.go b/models/migrations/v41.go index 4de3ad4e99..928bb1cd3f 100644 --- a/models/migrations/v41.go +++ b/models/migrations/v41.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func removeDuplicateUnitTypes(x *xorm.Engine) error { diff --git a/models/migrations/v45.go b/models/migrations/v45.go index 99baff2c8b..eb346d7b3a 100644 --- a/models/migrations/v45.go +++ b/models/migrations/v45.go @@ -8,7 +8,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func removeIndexColumnFromRepoUnitTable(x *xorm.Engine) (err error) { diff --git a/models/migrations/v46.go b/models/migrations/v46.go index b6dd059c94..3d9c1329d8 100644 --- a/models/migrations/v46.go +++ b/models/migrations/v46.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func removeOrganizationWatchRepo(x *xorm.Engine) error { diff --git a/models/migrations/v47.go b/models/migrations/v47.go index 7a217e6f01..81f92e2f5a 100644 --- a/models/migrations/v47.go +++ b/models/migrations/v47.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addDeletedBranch(x *xorm.Engine) (err error) { diff --git a/models/migrations/v48.go b/models/migrations/v48.go index 6cea66b5ac..6365feba89 100644 --- a/models/migrations/v48.go +++ b/models/migrations/v48.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addRepoIndexerStatus(x *xorm.Engine) error { diff --git a/models/migrations/v49.go b/models/migrations/v49.go index 9e98de5cf2..4776125137 100644 --- a/models/migrations/v49.go +++ b/models/migrations/v49.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addTimetracking(x *xorm.Engine) error { diff --git a/models/migrations/v50.go b/models/migrations/v50.go index 23b1bb526e..ddc378b432 100644 --- a/models/migrations/v50.go +++ b/models/migrations/v50.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func migrateProtectedBranchStruct(x *xorm.Engine) error { diff --git a/models/migrations/v51.go b/models/migrations/v51.go index 85e903bbe7..8dadcf3349 100644 --- a/models/migrations/v51.go +++ b/models/migrations/v51.go @@ -8,7 +8,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addDefaultValueToUserProhibitLogin(x *xorm.Engine) (err error) { diff --git a/models/migrations/v52.go b/models/migrations/v52.go index ab57d27de0..6547698d5b 100644 --- a/models/migrations/v52.go +++ b/models/migrations/v52.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/models" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addLFSLock(x *xorm.Engine) error { diff --git a/models/migrations/v53.go b/models/migrations/v53.go index 7437cace25..a3068cdb00 100644 --- a/models/migrations/v53.go +++ b/models/migrations/v53.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addReactions(x *xorm.Engine) error { diff --git a/models/migrations/v54.go b/models/migrations/v54.go index 5194624f69..af1e287419 100644 --- a/models/migrations/v54.go +++ b/models/migrations/v54.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addPullRequestOptions(x *xorm.Engine) error { diff --git a/models/migrations/v55.go b/models/migrations/v55.go index c20c51616e..a259e4f001 100644 --- a/models/migrations/v55.go +++ b/models/migrations/v55.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addModeToDeploKeys(x *xorm.Engine) error { diff --git a/models/migrations/v56.go b/models/migrations/v56.go index 79f8ce0ba5..4e1cafcca2 100644 --- a/models/migrations/v56.go +++ b/models/migrations/v56.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) { diff --git a/models/migrations/v57.go b/models/migrations/v57.go index fe4bf6b0ee..6c0ab6f496 100644 --- a/models/migrations/v57.go +++ b/models/migrations/v57.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addIssueClosedTime(x *xorm.Engine) error { diff --git a/models/migrations/v58.go b/models/migrations/v58.go index 6ec24b08c8..0fa3bcfe2d 100644 --- a/models/migrations/v58.go +++ b/models/migrations/v58.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addLabelsDescriptions(x *xorm.Engine) error { diff --git a/models/migrations/v59.go b/models/migrations/v59.go index 0a05495e76..d442f2569e 100644 --- a/models/migrations/v59.go +++ b/models/migrations/v59.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addProtectedBranchMergeWhitelist(x *xorm.Engine) error { diff --git a/models/migrations/v60.go b/models/migrations/v60.go index 13ec38241a..6482e8e4a5 100644 --- a/models/migrations/v60.go +++ b/models/migrations/v60.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addFsckEnabledToRepo(x *xorm.Engine) error { diff --git a/models/migrations/v61.go b/models/migrations/v61.go index 8d9b7e2d23..13affaf068 100644 --- a/models/migrations/v61.go +++ b/models/migrations/v61.go @@ -12,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addSizeToAttachment(x *xorm.Engine) error { diff --git a/models/migrations/v62.go b/models/migrations/v62.go index 0c2966854b..e7f6cf6890 100644 --- a/models/migrations/v62.go +++ b/models/migrations/v62.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addLastUsedPasscodeTOTP(x *xorm.Engine) error { diff --git a/models/migrations/v63.go b/models/migrations/v63.go index 6e7d940edc..62e8a299f6 100644 --- a/models/migrations/v63.go +++ b/models/migrations/v63.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addLanguageSetting(x *xorm.Engine) error { diff --git a/models/migrations/v64.go b/models/migrations/v64.go index 00637ca046..623cceddbc 100644 --- a/models/migrations/v64.go +++ b/models/migrations/v64.go @@ -7,7 +7,7 @@ package migrations import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addMultipleAssignees(x *xorm.Engine) error { diff --git a/models/migrations/v65.go b/models/migrations/v65.go index cc199d34e2..a87f8bc76c 100644 --- a/models/migrations/v65.go +++ b/models/migrations/v65.go @@ -3,7 +3,7 @@ package migrations import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addU2FReg(x *xorm.Engine) error { diff --git a/models/migrations/v66.go b/models/migrations/v66.go index 43acfb4ea5..8e9df97fea 100644 --- a/models/migrations/v66.go +++ b/models/migrations/v66.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addLoginSourceIDToPublicKeyTable(x *xorm.Engine) error { diff --git a/models/migrations/v67.go b/models/migrations/v67.go index 6cf3dd4d19..dee744e4d3 100644 --- a/models/migrations/v67.go +++ b/models/migrations/v67.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func removeStaleWatches(x *xorm.Engine) error { diff --git a/models/migrations/v68.go b/models/migrations/v68.go index d9e80ca80e..41c1f8f71d 100644 --- a/models/migrations/v68.go +++ b/models/migrations/v68.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/log" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) diff --git a/models/migrations/v69.go b/models/migrations/v69.go index 9a6e42e712..a08747edff 100644 --- a/models/migrations/v69.go +++ b/models/migrations/v69.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func moveTeamUnitsToTeamUnitTable(x *xorm.Engine) error { diff --git a/models/migrations/v70.go b/models/migrations/v70.go index 4ce1d4ee53..ef8dd85d6d 100644 --- a/models/migrations/v70.go +++ b/models/migrations/v70.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addIssueDependencies(x *xorm.Engine) (err error) { diff --git a/models/migrations/v71.go b/models/migrations/v71.go index 004f0a3f51..0b6aff61b2 100644 --- a/models/migrations/v71.go +++ b/models/migrations/v71.go @@ -11,8 +11,8 @@ import ( "code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "golang.org/x/crypto/pbkdf2" + "xorm.io/xorm" ) func addScratchHash(x *xorm.Engine) error { diff --git a/models/migrations/v72.go b/models/migrations/v72.go index c99b46afd2..612f58aab5 100644 --- a/models/migrations/v72.go +++ b/models/migrations/v72.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addReview(x *xorm.Engine) error { diff --git a/models/migrations/v73.go b/models/migrations/v73.go index 1265b4519e..0c06e2ba5c 100644 --- a/models/migrations/v73.go +++ b/models/migrations/v73.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addMustChangePassword(x *xorm.Engine) error { diff --git a/models/migrations/v74.go b/models/migrations/v74.go index 66e958c7fa..f3b38418b7 100644 --- a/models/migrations/v74.go +++ b/models/migrations/v74.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addApprovalWhitelistsToProtectedBranches(x *xorm.Engine) error { type ProtectedBranch struct { diff --git a/models/migrations/v75.go b/models/migrations/v75.go index 58d1d34c98..208153b9b0 100644 --- a/models/migrations/v75.go +++ b/models/migrations/v75.go @@ -5,8 +5,8 @@ package migrations import ( - "github.com/go-xorm/xorm" "xorm.io/builder" + "xorm.io/xorm" ) func clearNonusedData(x *xorm.Engine) error { diff --git a/models/migrations/v76.go b/models/migrations/v76.go index e1fd6f100b..545bff64c5 100644 --- a/models/migrations/v76.go +++ b/models/migrations/v76.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addPullRequestRebaseWithMerge(x *xorm.Engine) error { diff --git a/models/migrations/v77.go b/models/migrations/v77.go index c564d4cf54..d62fbe7fb6 100644 --- a/models/migrations/v77.go +++ b/models/migrations/v77.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addUserDefaultTheme(x *xorm.Engine) error { diff --git a/models/migrations/v78.go b/models/migrations/v78.go index 8082996b6f..e4274ca605 100644 --- a/models/migrations/v78.go +++ b/models/migrations/v78.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func renameRepoIsBareToIsEmpty(x *xorm.Engine) error { diff --git a/models/migrations/v79.go b/models/migrations/v79.go index e246393957..3c3e77b8db 100644 --- a/models/migrations/v79.go +++ b/models/migrations/v79.go @@ -7,7 +7,7 @@ package migrations import ( "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addCanCloseIssuesViaCommitInAnyBranch(x *xorm.Engine) error { diff --git a/models/migrations/v80.go b/models/migrations/v80.go index d9040da601..3c1b3315cf 100644 --- a/models/migrations/v80.go +++ b/models/migrations/v80.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addIsLockedToIssues(x *xorm.Engine) error { // Issue see models/issue.go diff --git a/models/migrations/v81.go b/models/migrations/v81.go index 48e96508d9..271d479a64 100644 --- a/models/migrations/v81.go +++ b/models/migrations/v81.go @@ -7,7 +7,7 @@ package migrations import ( "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func changeU2FCounterType(x *xorm.Engine) error { diff --git a/models/migrations/v82.go b/models/migrations/v82.go index eb73f18343..3fb4b6c59e 100644 --- a/models/migrations/v82.go +++ b/models/migrations/v82.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func fixReleaseSha1OnReleaseTable(x *xorm.Engine) error { diff --git a/models/migrations/v83.go b/models/migrations/v83.go index cdc59292ab..6707dbdf81 100644 --- a/models/migrations/v83.go +++ b/models/migrations/v83.go @@ -7,7 +7,7 @@ package migrations import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addUploaderIDForAttachment(x *xorm.Engine) error { diff --git a/models/migrations/v84.go b/models/migrations/v84.go index 4acb94b9ce..baab29fcd7 100644 --- a/models/migrations/v84.go +++ b/models/migrations/v84.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addGPGKeyImport(x *xorm.Engine) error { diff --git a/models/migrations/v85.go b/models/migrations/v85.go index 6066d5ebe9..8c92f10b6e 100644 --- a/models/migrations/v85.go +++ b/models/migrations/v85.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func hashAppToken(x *xorm.Engine) error { diff --git a/models/migrations/v86.go b/models/migrations/v86.go index 492a08c71e..39c196ca6a 100644 --- a/models/migrations/v86.go +++ b/models/migrations/v86.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addHTTPMethodToWebhook(x *xorm.Engine) error { diff --git a/models/migrations/v87.go b/models/migrations/v87.go index c8c7011a08..6b5af5be33 100644 --- a/models/migrations/v87.go +++ b/models/migrations/v87.go @@ -5,7 +5,7 @@ package migrations import ( - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func addAvatarFieldToRepository(x *xorm.Engine) error { diff --git a/models/migrations/v88.go b/models/migrations/v88.go index fef425db0a..7318995a8c 100644 --- a/models/migrations/v88.go +++ b/models/migrations/v88.go @@ -8,7 +8,7 @@ import ( "crypto/sha1" "fmt" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func hashContext(context string) string { diff --git a/models/migrations/v89.go b/models/migrations/v89.go index 83d0b1a8b9..a972b07b6d 100644 --- a/models/migrations/v89.go +++ b/models/migrations/v89.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addOriginalMigrationInfo(x *xorm.Engine) error { // Issue see models/issue.go diff --git a/models/migrations/v90.go b/models/migrations/v90.go index 09aceae2f9..72f7534dc8 100644 --- a/models/migrations/v90.go +++ b/models/migrations/v90.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func changeSomeColumnsLengthOfRepo(x *xorm.Engine) error { type Repository struct { diff --git a/models/migrations/v91.go b/models/migrations/v91.go index fea71b5d3b..3c49d9b96a 100644 --- a/models/migrations/v91.go +++ b/models/migrations/v91.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addIndexOnRepositoryAndComment(x *xorm.Engine) error { type Repository struct { diff --git a/models/migrations/v92.go b/models/migrations/v92.go index 090332f151..7ad5118176 100644 --- a/models/migrations/v92.go +++ b/models/migrations/v92.go @@ -5,8 +5,8 @@ package migrations import ( - "github.com/go-xorm/xorm" "xorm.io/builder" + "xorm.io/xorm" ) func removeLingeringIndexStatus(x *xorm.Engine) error { diff --git a/models/migrations/v93.go b/models/migrations/v93.go index 0b0441cd5d..0cb9d6631f 100644 --- a/models/migrations/v93.go +++ b/models/migrations/v93.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addEmailNotificationEnabledToUser(x *xorm.Engine) error { // User see models/user.go diff --git a/models/migrations/v94.go b/models/migrations/v94.go index 5fe8c3fa12..8c1e33b647 100644 --- a/models/migrations/v94.go +++ b/models/migrations/v94.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addStatusCheckColumnsForProtectedBranches(x *xorm.Engine) error { type ProtectedBranch struct { diff --git a/models/migrations/v95.go b/models/migrations/v95.go index f6e4e41c48..94787f7501 100644 --- a/models/migrations/v95.go +++ b/models/migrations/v95.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addCrossReferenceColumns(x *xorm.Engine) error { // Comment see models/comment.go diff --git a/models/migrations/v96.go b/models/migrations/v96.go index 5c2135ffc6..b8eb201591 100644 --- a/models/migrations/v96.go +++ b/models/migrations/v96.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) func deleteOrphanedAttachments(x *xorm.Engine) error { @@ -27,7 +27,7 @@ func deleteOrphanedAttachments(x *xorm.Engine) error { defer sess.Close() err := sess.BufferSize(setting.Database.IterateBufferSize). - Where("`comment_id` = 0 and (`release_id` = 0 or `release_id` not in (select `id` from `release`))").Cols("uuid"). + Where("`issue_id` = 0 and (`release_id` = 0 or `release_id` not in (select `id` from `release`))").Cols("uuid"). Iterate(new(Attachment), func(idx int, bean interface{}) error { attachment := bean.(*Attachment) diff --git a/models/migrations/v97.go b/models/migrations/v97.go index fa542f2ccd..8e58886e2e 100644 --- a/models/migrations/v97.go +++ b/models/migrations/v97.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addRepoAdminChangeTeamAccessColumnForUser(x *xorm.Engine) error { type User struct { diff --git a/models/migrations/v98.go b/models/migrations/v98.go index 3b9fdbb1c5..617e1ec3d7 100644 --- a/models/migrations/v98.go +++ b/models/migrations/v98.go @@ -4,7 +4,7 @@ package migrations -import "github.com/go-xorm/xorm" +import "xorm.io/xorm" func addOriginalAuthorOnMigratedReleases(x *xorm.Engine) error { type Release struct { diff --git a/models/migrations/v99.go b/models/migrations/v99.go index 70a32f6566..00b4509721 100644 --- a/models/migrations/v99.go +++ b/models/migrations/v99.go @@ -5,21 +5,30 @@ package migrations import ( - "github.com/go-xorm/xorm" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" ) -func addTeamIncludesAllRepositories(x *xorm.Engine) error { - - type Team struct { - ID int64 `xorm:"pk autoincr"` - IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"` +func addTaskTable(x *xorm.Engine) error { + type Task struct { + ID int64 + DoerID int64 `xorm:"index"` // operator + OwnerID int64 `xorm:"index"` // repo owner id, when creating, the repoID maybe zero + RepoID int64 `xorm:"index"` + Type structs.TaskType + Status structs.TaskStatus `xorm:"index"` + StartTime timeutil.TimeStamp + EndTime timeutil.TimeStamp + PayloadContent string `xorm:"TEXT"` + Errors string `xorm:"TEXT"` // if task failed, saved the error reason + Created timeutil.TimeStamp `xorm:"created"` } - if err := x.Sync2(new(Team)); err != nil { - return err + type Repository struct { + Status int `xorm:"NOT NULL DEFAULT 0"` } - _, err := x.Exec("UPDATE `team` SET `includes_all_repositories` = ? WHERE `name`=?", - true, "Owners") - return err + return x.Sync2(new(Task), new(Repository)) } diff --git a/models/models.go b/models/models.go index e802a35a77..854cb33b14 100644 --- a/models/models.go +++ b/models/models.go @@ -14,8 +14,8 @@ import ( // Needed for the MySQL driver _ "github.com/go-sql-driver/mysql" - "github.com/go-xorm/xorm" "xorm.io/core" + "xorm.io/xorm" // Needed for the Postgresql driver _ "github.com/lib/pq" @@ -112,6 +112,7 @@ func init() { new(OAuth2Application), new(OAuth2AuthorizationCode), new(OAuth2Grant), + new(Task), ) gonicNames := []string{"SSL", "UID"} @@ -156,11 +157,9 @@ func SetEngine() (err error) { // so use log file to instead print to stdout. x.SetLogger(NewXORMLogger(setting.Database.LogSQL)) x.ShowSQL(setting.Database.LogSQL) - if setting.Database.UseMySQL { - x.SetMaxIdleConns(setting.Database.MaxIdleConns) - x.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) - } - + x.SetMaxOpenConns(setting.Database.MaxOpenConns) + x.SetMaxIdleConns(setting.Database.MaxIdleConns) + x.SetConnMaxLifetime(setting.Database.ConnMaxLifetime) return nil } diff --git a/models/oauth2_application.go b/models/oauth2_application.go index 46355a0b3f..4df207ae16 100644 --- a/models/oauth2_application.go +++ b/models/oauth2_application.go @@ -16,10 +16,10 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/dgrijalva/jwt-go" - "github.com/go-xorm/xorm" uuid "github.com/satori/go.uuid" "github.com/unknwon/com" "golang.org/x/crypto/bcrypt" + "xorm.io/xorm" ) // OAuth2Application represents an OAuth2 client (RFC 6749) diff --git a/models/org.go b/models/org.go index 07daeaeacc..78b035b101 100644 --- a/models/org.go +++ b/models/org.go @@ -14,9 +14,9 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "github.com/go-xorm/xorm" "github.com/unknwon/com" "xorm.io/builder" + "xorm.io/xorm" ) // IsOwnedBy returns true if given user is in the owner team. diff --git a/models/org_team.go b/models/org_team.go index 7430c65e4d..8ba5d2742d 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -14,8 +14,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" "xorm.io/builder" + "xorm.io/xorm" ) const ownerTeamName = "Owners" @@ -341,7 +341,7 @@ func (t *Team) UnitEnabled(tp UnitType) bool { func (t *Team) unitEnabled(e Engine, tp UnitType) bool { if err := t.getUnits(e); err != nil { - log.Warn("Error loading repository (ID: %d) units: %s", t.ID, err.Error()) + log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error()) } for _, unit := range t.Units { @@ -766,7 +766,7 @@ func AddTeamMember(team *Team, userID int64) error { // Give access to team repositories. for _, repo := range team.Repos { - if err := repo.recalculateTeamAccesses(sess, 0); err != nil { + if err := repo.recalculateUserAccess(sess, userID); err != nil { return err } if setting.Service.AutoWatchNewRepos { @@ -811,7 +811,7 @@ func removeTeamMember(e *xorm.Session, team *Team, userID int64) error { // Delete access to team repositories. for _, repo := range team.Repos { - if err := repo.recalculateTeamAccesses(e, 0); err != nil { + if err := repo.recalculateUserAccess(e, userID); err != nil { return err } diff --git a/models/pull.go b/models/pull.go index ff1f7b773b..c6da63ec55 100644 --- a/models/pull.go +++ b/models/pull.go @@ -25,8 +25,8 @@ import ( "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "github.com/unknwon/com" + "xorm.io/xorm" ) var pullRequestQueue = sync.NewUniqueQueue(setting.Repository.PullRequestQueueLength) @@ -66,7 +66,6 @@ type PullRequest struct { HeadRepo *Repository `xorm:"-"` BaseRepoID int64 `xorm:"INDEX"` BaseRepo *Repository `xorm:"-"` - HeadUserName string HeadBranch string BaseBranch string ProtectedBranch *ProtectedBranch `xorm:"-"` @@ -79,6 +78,15 @@ type PullRequest struct { MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"` } +// MustHeadUserName returns the HeadRepo's username if failed return blank +func (pr *PullRequest) MustHeadUserName() string { + if err := pr.LoadHeadRepo(); err != nil { + log.Error("LoadHeadRepo: %v", err) + return "" + } + return pr.HeadRepo.MustOwnerName() +} + // Note: don't try to get Issue because will end up recursive querying. func (pr *PullRequest) loadAttributes(e Engine) (err error) { if pr.HasMerged && pr.Merger == nil { @@ -102,6 +110,10 @@ func (pr *PullRequest) LoadAttributes() error { // LoadBaseRepo loads pull request base repository from database func (pr *PullRequest) LoadBaseRepo() error { if pr.BaseRepo == nil { + if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil { + pr.BaseRepo = pr.HeadRepo + return nil + } var repo Repository if has, err := x.ID(pr.BaseRepoID).Get(&repo); err != nil { return err @@ -113,6 +125,24 @@ func (pr *PullRequest) LoadBaseRepo() error { return nil } +// LoadHeadRepo loads pull request head repository from database +func (pr *PullRequest) LoadHeadRepo() error { + if pr.HeadRepo == nil { + if pr.HeadRepoID == pr.BaseRepoID && pr.BaseRepo != nil { + pr.HeadRepo = pr.BaseRepo + return nil + } + var repo Repository + if has, err := x.ID(pr.HeadRepoID).Get(&repo); err != nil { + return err + } else if !has { + return ErrRepoNotExist{ID: pr.BaseRepoID} + } + pr.HeadRepo = &repo + } + return nil +} + // LoadIssue loads issue information from database func (pr *PullRequest) LoadIssue() (err error) { return pr.loadIssue(x) @@ -152,7 +182,7 @@ func (pr *PullRequest) GetDefaultMergeMessage() string { return "" } } - return fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch) + return fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.MustHeadUserName(), pr.HeadRepo.Name, pr.BaseBranch) } // GetDefaultSquashMessage returns default message used when squash and merging pull request @@ -656,11 +686,11 @@ func (pr *PullRequest) testPatch(e Engine) (err error) { } // NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte, assigneeIDs []int64) (err error) { +func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) { // Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887 i := 0 for { - if err = newPullRequestAttempt(repo, pull, labelIDs, uuids, pr, patch, assigneeIDs); err == nil { + if err = newPullRequestAttempt(repo, pull, labelIDs, uuids, pr, patch); err == nil { return nil } if !IsErrNewIssueInsert(err) { @@ -674,7 +704,7 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str return fmt.Errorf("NewPullRequest: too many errors attempting to insert the new issue. Last error was: %v", err) } -func newPullRequestAttempt(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte, assigneeIDs []int64) (err error) { +func newPullRequestAttempt(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -687,7 +717,6 @@ func newPullRequestAttempt(repo *Repository, pull *Issue, labelIDs []int64, uuid LabelIDs: labelIDs, Attachments: uuids, IsPull: true, - AssigneeIDs: assigneeIDs, }); err != nil { if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { return err @@ -1072,105 +1101,6 @@ func (prs PullRequestList) InvalidateCodeComments(doer *User, repo *git.Reposito return prs.invalidateCodeComments(x, doer, repo, branch) } -func addHeadRepoTasks(prs []*PullRequest) { - for _, pr := range prs { - log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID) - if err := pr.UpdatePatch(); err != nil { - log.Error("UpdatePatch: %v", err) - continue - } else if err := pr.PushToBaseRepo(); err != nil { - log.Error("PushToBaseRepo: %v", err) - continue - } - - pr.AddToTaskQueue() - } -} - -// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch, -// and generate new patch for testing as needed. -func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool) { - log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch) - prs, err := GetUnmergedPullRequestsByHeadInfo(repoID, branch) - if err != nil { - log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err) - return - } - - if isSync { - requests := PullRequestList(prs) - if err = requests.LoadAttributes(); err != nil { - log.Error("PullRequestList.LoadAttributes: %v", err) - } - if invalidationErr := checkForInvalidation(requests, repoID, doer, branch); invalidationErr != nil { - log.Error("checkForInvalidation: %v", invalidationErr) - } - if err == nil { - for _, pr := range prs { - pr.Issue.PullRequest = pr - if err = pr.Issue.LoadAttributes(); err != nil { - log.Error("LoadAttributes: %v", err) - continue - } - if err = PrepareWebhooks(pr.Issue.Repo, HookEventPullRequest, &api.PullRequestPayload{ - Action: api.HookIssueSynchronized, - Index: pr.Issue.Index, - PullRequest: pr.Issue.PullRequest.APIFormat(), - Repository: pr.Issue.Repo.APIFormat(AccessModeNone), - Sender: doer.APIFormat(), - }); err != nil { - log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err) - continue - } - go HookQueue.Add(pr.Issue.Repo.ID) - } - } - - } - - addHeadRepoTasks(prs) - - log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch) - prs, err = GetUnmergedPullRequestsByBaseInfo(repoID, branch) - if err != nil { - log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err) - return - } - for _, pr := range prs { - pr.AddToTaskQueue() - } -} - -func checkForInvalidation(requests PullRequestList, repoID int64, doer *User, branch string) error { - repo, err := GetRepositoryByID(repoID) - if err != nil { - return fmt.Errorf("GetRepositoryByID: %v", err) - } - gitRepo, err := git.OpenRepository(repo.RepoPath()) - if err != nil { - return fmt.Errorf("git.OpenRepository: %v", err) - } - go func() { - err := requests.InvalidateCodeComments(doer, gitRepo, branch) - if err != nil { - log.Error("PullRequestList.InvalidateCodeComments: %v", err) - } - }() - return nil -} - -// ChangeUsernameInPullRequests changes the name of head_user_name -func ChangeUsernameInPullRequests(oldUserName, newUserName string) error { - pr := PullRequest{ - HeadUserName: strings.ToLower(newUserName), - } - _, err := x. - Cols("head_user_name"). - Where("head_user_name = ?", strings.ToLower(oldUserName)). - Update(pr) - return err -} - // checkAndUpdateStatus checks if pull request is possible to leaving checking status, // and set to be either conflict or mergeable. func (pr *PullRequest) checkAndUpdateStatus() { diff --git a/models/pull_test.go b/models/pull_test.go index df051d51bc..8e2436b1a2 100644 --- a/models/pull_test.go +++ b/models/pull_test.go @@ -232,20 +232,6 @@ func TestPullRequestList_LoadAttributes(t *testing.T) { // TODO TestAddTestPullRequestTask -func TestChangeUsernameInPullRequests(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - const newUsername = "newusername" - assert.NoError(t, ChangeUsernameInPullRequests("user1", newUsername)) - - prs := make([]*PullRequest, 0, 10) - assert.NoError(t, x.Where("head_user_name = ?", newUsername).Find(&prs)) - assert.Len(t, prs, 2) - for _, pr := range prs { - assert.Equal(t, newUsername, pr.HeadUserName) - } - CheckConsistencyFor(t, &PullRequest{}) -} - func TestPullRequest_IsWorkInProgress(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) diff --git a/models/release.go b/models/release.go index 243cc2fa3c..f43d81d822 100644 --- a/models/release.go +++ b/models/release.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -366,3 +367,16 @@ func SyncReleasesWithTags(repo *Repository, gitRepo *git.Repository) error { } return nil } + +// UpdateReleasesMigrationsByType updates all migrated repositories' releases from gitServiceType to replace originalAuthorID to posterID +func UpdateReleasesMigrationsByType(gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error { + _, err := x.Table("release"). + Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). + And("original_author_id = ?", originalAuthorID). + Update(map[string]interface{}{ + "publisher_id": posterID, + "original_author": "", + "original_author_id": 0, + }) + return err +} diff --git a/models/repo.go b/models/repo.go index 53d568e70c..d90cdeb77f 100644 --- a/models/repo.go +++ b/models/repo.go @@ -32,14 +32,16 @@ import ( "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "github.com/mcuadros/go-version" "github.com/unknwon/com" ini "gopkg.in/ini.v1" "xorm.io/builder" + "xorm.io/xorm" ) var repoWorkingPool = sync.NewExclusivePool() @@ -126,18 +128,28 @@ func NewRepoContext() { RemoveAllWithNotice("Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp")) } +// RepositoryStatus defines the status of repository +type RepositoryStatus int + +// all kinds of RepositoryStatus +const ( + RepositoryReady RepositoryStatus = iota // a normal repository + RepositoryBeingMigrated // repository is migrating +) + // Repository represents a git repository. type Repository struct { - ID int64 `xorm:"pk autoincr"` - OwnerID int64 `xorm:"UNIQUE(s) index"` - OwnerName string `xorm:"-"` - Owner *User `xorm:"-"` - LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string `xorm:"INDEX NOT NULL"` - Description string `xorm:"TEXT"` - Website string `xorm:"VARCHAR(2048)"` - OriginalURL string `xorm:"VARCHAR(2048)"` - DefaultBranch string + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(s) index"` + OwnerName string `xorm:"-"` + Owner *User `xorm:"-"` + LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string `xorm:"INDEX NOT NULL"` + Description string `xorm:"TEXT"` + Website string `xorm:"VARCHAR(2048)"` + OriginalServiceType structs.GitServiceType `xorm:"index"` + OriginalURL string `xorm:"VARCHAR(2048)"` + DefaultBranch string NumWatches int NumStars int @@ -156,9 +168,9 @@ type Repository struct { IsPrivate bool `xorm:"INDEX"` IsEmpty bool `xorm:"INDEX"` IsArchived bool `xorm:"INDEX"` - - IsMirror bool `xorm:"INDEX"` - *Mirror `xorm:"-"` + IsMirror bool `xorm:"INDEX"` + *Mirror `xorm:"-"` + Status RepositoryStatus `xorm:"NOT NULL DEFAULT 0"` ExternalMetas map[string]string `xorm:"-"` Units []*RepoUnit `xorm:"-"` @@ -197,6 +209,16 @@ func (repo *Repository) ColorFormat(s fmt.State) { repo.Name) } +// IsBeingMigrated indicates that repository is being migtated +func (repo *Repository) IsBeingMigrated() bool { + return repo.Status == RepositoryBeingMigrated +} + +// IsBeingCreated indicates that repository is being migrated or forked +func (repo *Repository) IsBeingCreated() bool { + return repo.IsBeingMigrated() +} + // AfterLoad is invoked from XORM after setting the values of all fields of this object. func (repo *Repository) AfterLoad() { // FIXME: use models migration to solve all at once. @@ -735,11 +757,24 @@ func (repo *Repository) CanEnableEditor() bool { return !repo.IsMirror } +// GetReaders returns all users that have explicit read access or higher to the repository. +func (repo *Repository) GetReaders() (_ []*User, err error) { + return repo.getUsersWithAccessMode(x, AccessModeRead) +} + // GetWriters returns all users that have write access to the repository. func (repo *Repository) GetWriters() (_ []*User, err error) { return repo.getUsersWithAccessMode(x, AccessModeWrite) } +// IsReader returns true if user has explicit read access or higher to the repository. +func (repo *Repository) IsReader(userID int64) (bool, error) { + if repo.OwnerID == userID { + return true, nil + } + return x.Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, AccessModeRead).Get(&Access{}) +} + // getUsersWithAccessMode returns users that have at least given access mode to the repository. func (repo *Repository) getUsersWithAccessMode(e Engine, mode AccessMode) (_ []*User, err error) { if err = repo.getOwner(e); err != nil { @@ -871,18 +906,6 @@ func (repo *Repository) CloneLink() (cl *CloneLink) { return repo.cloneLink(x, false) } -// MigrateRepoOptions contains the repository migrate options -type MigrateRepoOptions struct { - Name string - Description string - OriginalURL string - IsPrivate bool - IsMirror bool - RemoteAddr string - Wiki bool // include wiki repository - SyncReleasesWithTags bool // sync releases from tags -} - /* GitHub, GitLab, Gogs: *.wiki.git BitBucket: *.git/wiki @@ -902,20 +925,28 @@ func wikiRemoteURL(remote string) string { return "" } -// MigrateRepository migrates an existing repository from other project hosting. -func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, error) { - repo, err := CreateRepository(doer, u, CreateRepoOptions{ - Name: opts.Name, - Description: opts.Description, - OriginalURL: opts.OriginalURL, - IsPrivate: opts.IsPrivate, - IsMirror: opts.IsMirror, - }) - if err != nil { - return nil, err +// CheckCreateRepository check if could created a repository +func CheckCreateRepository(doer, u *User, name string) error { + if !doer.CanCreateRepo() { + return ErrReachLimitOfRepo{u.MaxRepoCreation} } - repoPath := RepoPath(u.Name, opts.Name) + if err := IsUsableRepoName(name); err != nil { + return err + } + + has, err := isRepositoryExist(x, u, name) + if err != nil { + return fmt.Errorf("IsRepositoryExist: %v", err) + } else if has { + return ErrRepoAlreadyExist{u.Name, name} + } + return nil +} + +// MigrateRepositoryGitData starts migrating git related data after created migrating repository +func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts api.MigrateRepoOption) (*Repository, error) { + repoPath := RepoPath(u.Name, opts.RepoName) if u.IsOrganization() { t, err := u.GetOwnerTeam() @@ -929,11 +960,12 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second - if err := os.RemoveAll(repoPath); err != nil { + var err error + if err = os.RemoveAll(repoPath); err != nil { return repo, fmt.Errorf("Failed to remove %s: %v", repoPath, err) } - if err = git.Clone(opts.RemoteAddr, repoPath, git.CloneRepoOptions{ + if err = git.Clone(opts.CloneAddr, repoPath, git.CloneRepoOptions{ Mirror: true, Quiet: true, Timeout: migrateTimeout, @@ -942,8 +974,8 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err } if opts.Wiki { - wikiPath := WikiPath(u.Name, opts.Name) - wikiRemotePath := wikiRemoteURL(opts.RemoteAddr) + wikiPath := WikiPath(u.Name, opts.RepoName) + wikiRemotePath := wikiRemoteURL(opts.CloneAddr) if len(wikiRemotePath) > 0 { if err := os.RemoveAll(wikiPath); err != nil { return repo, fmt.Errorf("Failed to remove %s: %v", wikiPath, err) @@ -973,7 +1005,7 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err return repo, fmt.Errorf("git.IsEmpty: %v", err) } - if opts.SyncReleasesWithTags && !repo.IsEmpty { + if !opts.Releases && !repo.IsEmpty { // Try to get HEAD branch and set it as default branch. headBranch, err := gitRepo.GetHEADBranch() if err != nil { @@ -992,7 +1024,7 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err log.Error("Failed to update size for repository: %v", err) } - if opts.IsMirror { + if opts.Mirror { if _, err = x.InsertOne(&Mirror{ RepoID: repo.ID, Interval: setting.Mirror.DefaultInterval, @@ -1080,7 +1112,7 @@ func CleanUpMigrateInfo(repo *Repository) (*Repository, error) { } } - _, err := git.NewCommand("remote", "remove", "origin").RunInDir(repoPath) + _, err := git.NewCommand("remote", "rm", "origin").RunInDir(repoPath) if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { return repo, fmt.Errorf("CleanUpMigrateInfo: %v", err) } @@ -1095,7 +1127,20 @@ func CleanUpMigrateInfo(repo *Repository) (*Repository, error) { } // initRepoCommit temporarily changes with work directory. -func initRepoCommit(tmpPath string, sig *git.Signature) (err error) { +func initRepoCommit(tmpPath string, u *User) (err error) { + commitTimeStr := time.Now().Format(time.RFC3339) + + sig := u.NewGitSig() + // Because this may call hooks we should pass in the environment + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+sig.Name, + "GIT_AUTHOR_EMAIL="+sig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+sig.Name, + "GIT_COMMITTER_EMAIL="+sig.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + var stderr string if _, stderr, err = process.GetManager().ExecDir(-1, tmpPath, fmt.Sprintf("initRepoCommit (git add): %s", tmpPath), @@ -1103,10 +1148,29 @@ func initRepoCommit(tmpPath string, sig *git.Signature) (err error) { return fmt.Errorf("git add: %s", stderr) } - if _, stderr, err = process.GetManager().ExecDir(-1, + binVersion, err := git.BinVersion() + if err != nil { + return fmt.Errorf("Unable to get git version: %v", err) + } + + args := []string{ + "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), + "-m", "Initial commit", + } + + if version.Compare(binVersion, "1.7.9", ">=") { + sign, keyID := SignInitialCommit(tmpPath, u) + if sign { + args = append(args, "-S"+keyID) + } else if version.Compare(binVersion, "2.0.0", ">=") { + args = append(args, "--no-gpg-sign") + } + } + + if _, stderr, err = process.GetManager().ExecDirEnv(-1, tmpPath, fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath), - git.GitExecutable, "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), - "-m", "Initial commit"); err != nil { + env, + git.GitExecutable, args...); err != nil { return fmt.Errorf("git commit: %s", stderr) } @@ -1130,6 +1194,7 @@ type CreateRepoOptions struct { IsPrivate bool IsMirror bool AutoInit bool + Status RepositoryStatus } func getRepoInitFile(tp, name string) ([]byte, error) { @@ -1157,9 +1222,24 @@ func getRepoInitFile(tp, name string) ([]byte, error) { } func prepareRepoCommit(e Engine, repo *Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { + commitTimeStr := time.Now().Format(time.RFC3339) + authorSig := repo.Owner.NewGitSig() + + // Because this may call hooks we should pass in the environment + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+authorSig.Name, + "GIT_AUTHOR_EMAIL="+authorSig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+authorSig.Name, + "GIT_COMMITTER_EMAIL="+authorSig.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + // Clone to temporary path and do the init commit. - _, stderr, err := process.GetManager().Exec( + _, stderr, err := process.GetManager().ExecDirEnv( + -1, "", fmt.Sprintf("initRepository(git clone): %s", repoPath), + env, git.GitExecutable, "clone", repoPath, tmpDir, ) if err != nil { @@ -1250,7 +1330,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts C } // Apply changes and commit. - if err = initRepoCommit(tmpDir, u.NewGitSig()); err != nil { + if err = initRepoCommit(tmpDir, u); err != nil { return fmt.Errorf("initRepoCommit: %v", err) } } @@ -1402,6 +1482,8 @@ func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err IsPrivate: opts.IsPrivate, IsFsckEnabled: !opts.IsMirror, CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, + Status: opts.Status, + IsEmpty: !opts.AutoInit, } sess := x.NewSession() @@ -1852,6 +1934,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error { &CommitStatus{RepoID: repoID}, &RepoIndexerStatus{RepoID: repoID}, &Comment{RefRepoID: repoID}, + &Task{RepoID: repoID}, ); err != nil { return fmt.Errorf("deleteBeans: %v", err) } @@ -1942,12 +2025,11 @@ func DeleteRepository(doer *User, uid, repoID int64) error { if err != nil { return err } - if count > 1 { continue } - oidPath := filepath.Join(v.Oid[0:2], v.Oid[2:4], v.Oid[4:len(v.Oid)]) + oidPath := filepath.Join(setting.LFS.ContentPath, v.Oid[0:2], v.Oid[2:4], v.Oid[4:len(v.Oid)]) removeAllWithNotice(sess, "Delete orphaned LFS file", oidPath) } diff --git a/models/repo_activity.go b/models/repo_activity.go index 04612ae1ef..aa5c2217e0 100644 --- a/models/repo_activity.go +++ b/models/repo_activity.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/git" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // ActivityAuthorData represents statistical git commit count data diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go index 40ddf6a28c..3d6447c196 100644 --- a/models/repo_collaboration.go +++ b/models/repo_collaboration.go @@ -41,12 +41,7 @@ func (repo *Repository) AddCollaborator(u *User) error { return err } - if repo.Owner.IsOrganization() { - err = repo.recalculateTeamAccesses(sess, 0) - } else { - err = repo.recalculateAccesses(sess) - } - if err != nil { + if err = repo.recalculateUserAccess(sess, u.ID); err != nil { return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err) } @@ -89,6 +84,18 @@ func (repo *Repository) GetCollaborators() ([]*Collaborator, error) { return repo.getCollaborators(x) } +func (repo *Repository) getCollaboration(e Engine, uid int64) (*Collaboration, error) { + collaboration := &Collaboration{ + RepoID: repo.ID, + UserID: uid, + } + has, err := e.Get(collaboration) + if !has { + collaboration = nil + } + return collaboration, err +} + func (repo *Repository) isCollaborator(e Engine, userID int64) (bool, error) { return e.Get(&Collaboration{RepoID: repo.ID, UserID: userID}) } diff --git a/models/repo_indexer.go b/models/repo_indexer.go index b842a1c87f..9cc002a8ab 100644 --- a/models/repo_indexer.go +++ b/models/repo_indexer.go @@ -8,10 +8,12 @@ import ( "fmt" "strconv" "strings" + "time" "code.gitea.io/gitea/modules/base" "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.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -70,9 +72,30 @@ func InitRepoIndexer() { if !setting.Indexer.RepoIndexerEnabled { return } + waitChannel := make(chan time.Duration) repoIndexerOperationQueue = make(chan repoIndexerOperation, setting.Indexer.UpdateQueueLength) - indexer.InitRepoIndexer(populateRepoIndexerAsynchronously) - go processRepoIndexerOperationQueue() + go func() { + start := time.Now() + log.Info("Initializing Repository Indexer") + indexer.InitRepoIndexer(populateRepoIndexerAsynchronously) + go processRepoIndexerOperationQueue() + waitChannel <- time.Since(start) + }() + if setting.Indexer.StartupTimeout > 0 { + go func() { + timeout := setting.Indexer.StartupTimeout + if graceful.IsChild && setting.GracefulHammerTime > 0 { + timeout += setting.GracefulHammerTime + } + select { + case duration := <-waitChannel: + log.Info("Repository Indexer Initialization took %v", duration) + case <-time.After(timeout): + log.Fatal("Repository Indexer Initialization Timed-Out after: %v", timeout) + } + }() + + } } // populateRepoIndexerAsynchronously asynchronously populates the repo indexer diff --git a/models/repo_mirror.go b/models/repo_mirror.go index 4e91ea296a..aa0ec26808 100644 --- a/models/repo_mirror.go +++ b/models/repo_mirror.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" + "xorm.io/xorm" ) // Mirror represents mirror information of a repository. diff --git a/models/repo_permission.go b/models/repo_permission.go index 916678d168..fad29bd169 100644 --- a/models/repo_permission.go +++ b/models/repo_permission.go @@ -329,10 +329,18 @@ func HasAccessUnit(user *User, repo *Repository, unitType UnitType, testMode Acc return hasAccessUnit(x, user, repo, unitType, testMode) } -// canBeAssigned return true if user could be assigned to a repo +// CanBeAssigned return true if user can be assigned to issue or pull requests in repo +// Currently any write access (code, issues or pr's) is assignable, to match assignee list in user interface. // FIXME: user could send PullRequest also could be assigned??? -func canBeAssigned(e Engine, user *User, repo *Repository) (bool, error) { - return hasAccessUnit(e, user, repo, UnitTypeCode, AccessModeWrite) +func CanBeAssigned(user *User, repo *Repository, isPull bool) (bool, error) { + if user.IsOrganization() { + return false, fmt.Errorf("Organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) + } + perm, err := GetUserRepoPermission(repo, user) + if err != nil { + return false, err + } + return perm.CanAccessAny(AccessModeWrite, UnitTypeCode, UnitTypeIssues, UnitTypePullRequests), nil } func hasAccess(e Engine, userID int64, repo *Repository) (bool, error) { diff --git a/models/repo_sign.go b/models/repo_sign.go new file mode 100644 index 0000000000..bac69f76a8 --- /dev/null +++ b/models/repo_sign.go @@ -0,0 +1,303 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/process" + "code.gitea.io/gitea/modules/setting" +) + +type signingMode string + +const ( + never signingMode = "never" + always signingMode = "always" + pubkey signingMode = "pubkey" + twofa signingMode = "twofa" + parentSigned signingMode = "parentsigned" + baseSigned signingMode = "basesigned" + headSigned signingMode = "headsigned" + commitsSigned signingMode = "commitssigned" +) + +func signingModeFromStrings(modeStrings []string) []signingMode { + returnable := make([]signingMode, 0, len(modeStrings)) + for _, mode := range modeStrings { + signMode := signingMode(strings.ToLower(mode)) + switch signMode { + case never: + return []signingMode{never} + case always: + return []signingMode{always} + case pubkey: + fallthrough + case twofa: + fallthrough + case parentSigned: + fallthrough + case baseSigned: + fallthrough + case headSigned: + fallthrough + case commitsSigned: + returnable = append(returnable, signMode) + } + } + if len(returnable) == 0 { + return []signingMode{never} + } + return returnable +} + +func signingKey(repoPath string) string { + if setting.Repository.Signing.SigningKey == "none" { + return "" + } + + if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { + // Can ignore the error here as it means that commit.gpgsign is not set + value, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunInDir(repoPath) + sign, valid := git.ParseBool(strings.TrimSpace(value)) + if !sign || !valid { + return "" + } + + signingKey, _ := git.NewCommand("config", "--get", "user.signingkey").RunInDir(repoPath) + return strings.TrimSpace(signingKey) + } + + return setting.Repository.Signing.SigningKey +} + +// PublicSigningKey gets the public signing key within a provided repository directory +func PublicSigningKey(repoPath string) (string, error) { + signingKey := signingKey(repoPath) + if signingKey == "" { + return "", nil + } + + content, stderr, err := process.GetManager().ExecDir(-1, repoPath, + "gpg --export -a", "gpg", "--export", "-a", signingKey) + if err != nil { + log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err) + return "", err + } + return content, nil +} + +// SignInitialCommit determines if we should sign the initial commit to this repository +func SignInitialCommit(repoPath string, u *User) (bool, string) { + rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) + signingKey := signingKey(repoPath) + if signingKey == "" { + return false, "" + } + + for _, rule := range rules { + switch rule { + case never: + return false, "" + case always: + break + case pubkey: + keys, err := ListGPGKeys(u.ID) + if err != nil || len(keys) == 0 { + return false, "" + } + case twofa: + twofa, err := GetTwoFactorByUID(u.ID) + if err != nil || twofa == nil { + return false, "" + } + } + } + return true, signingKey +} + +// SignWikiCommit determines if we should sign the commits to this repository wiki +func (repo *Repository) SignWikiCommit(u *User) (bool, string) { + rules := signingModeFromStrings(setting.Repository.Signing.Wiki) + signingKey := signingKey(repo.WikiPath()) + if signingKey == "" { + return false, "" + } + + for _, rule := range rules { + switch rule { + case never: + return false, "" + case always: + break + case pubkey: + keys, err := ListGPGKeys(u.ID) + if err != nil || len(keys) == 0 { + return false, "" + } + case twofa: + twofa, err := GetTwoFactorByUID(u.ID) + if err != nil || twofa == nil { + return false, "" + } + case parentSigned: + gitRepo, err := git.OpenRepository(repo.WikiPath()) + if err != nil { + return false, "" + } + commit, err := gitRepo.GetCommit("HEAD") + if err != nil { + return false, "" + } + if commit.Signature == nil { + return false, "" + } + verification := ParseCommitWithSignature(commit) + if !verification.Verified { + return false, "" + } + } + } + return true, signingKey +} + +// SignCRUDAction determines if we should sign a CRUD commit to this repository +func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string) { + rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) + signingKey := signingKey(repo.RepoPath()) + if signingKey == "" { + return false, "" + } + + for _, rule := range rules { + switch rule { + case never: + return false, "" + case always: + break + case pubkey: + keys, err := ListGPGKeys(u.ID) + if err != nil || len(keys) == 0 { + return false, "" + } + case twofa: + twofa, err := GetTwoFactorByUID(u.ID) + if err != nil || twofa == nil { + return false, "" + } + case parentSigned: + gitRepo, err := git.OpenRepository(tmpBasePath) + if err != nil { + return false, "" + } + commit, err := gitRepo.GetCommit(parentCommit) + if err != nil { + return false, "" + } + if commit.Signature == nil { + return false, "" + } + verification := ParseCommitWithSignature(commit) + if !verification.Verified { + return false, "" + } + } + } + return true, signingKey +} + +// SignMerge determines if we should sign a merge commit to this repository +func (repo *Repository) SignMerge(u *User, tmpBasePath, baseCommit, headCommit string) (bool, string) { + rules := signingModeFromStrings(setting.Repository.Signing.Merges) + signingKey := signingKey(repo.RepoPath()) + if signingKey == "" { + return false, "" + } + var gitRepo *git.Repository + var err error + + for _, rule := range rules { + switch rule { + case never: + return false, "" + case always: + break + case pubkey: + keys, err := ListGPGKeys(u.ID) + if err != nil || len(keys) == 0 { + return false, "" + } + case twofa: + twofa, err := GetTwoFactorByUID(u.ID) + if err != nil || twofa == nil { + return false, "" + } + case baseSigned: + if gitRepo == nil { + gitRepo, err = git.OpenRepository(tmpBasePath) + if err != nil { + return false, "" + } + } + commit, err := gitRepo.GetCommit(baseCommit) + if err != nil { + return false, "" + } + verification := ParseCommitWithSignature(commit) + if !verification.Verified { + return false, "" + } + case headSigned: + if gitRepo == nil { + gitRepo, err = git.OpenRepository(tmpBasePath) + if err != nil { + return false, "" + } + } + commit, err := gitRepo.GetCommit(headCommit) + if err != nil { + return false, "" + } + verification := ParseCommitWithSignature(commit) + if !verification.Verified { + return false, "" + } + case commitsSigned: + if gitRepo == nil { + gitRepo, err = git.OpenRepository(tmpBasePath) + if err != nil { + return false, "" + } + } + commit, err := gitRepo.GetCommit(headCommit) + if err != nil { + return false, "" + } + verification := ParseCommitWithSignature(commit) + if !verification.Verified { + return false, "" + } + // need to work out merge-base + mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) + if err != nil { + return false, "" + } + commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) + if err != nil { + return false, "" + } + for e := commitList.Front(); e != nil; e = e.Next() { + commit = e.Value.(*git.Commit) + verification := ParseCommitWithSignature(commit) + if !verification.Verified { + return false, "" + } + } + } + } + return true, signingKey +} diff --git a/models/repo_unit.go b/models/repo_unit.go index 2fc1c40fa2..a6162a65e5 100644 --- a/models/repo_unit.go +++ b/models/repo_unit.go @@ -9,9 +9,9 @@ import ( "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "github.com/unknwon/com" "xorm.io/core" + "xorm.io/xorm" ) // RepoUnit describes all units of a repository diff --git a/models/review.go b/models/review.go index 454d16ee88..58660b2e3d 100644 --- a/models/review.go +++ b/models/review.go @@ -10,9 +10,9 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "xorm.io/builder" "xorm.io/core" + "xorm.io/xorm" ) // ReviewType defines the sort of feedback a review gives diff --git a/models/ssh_key.go b/models/ssh_key.go index b7c5b4fe6e..69699f24c1 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -28,10 +28,10 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" - "github.com/go-xorm/xorm" "github.com/unknwon/com" "golang.org/x/crypto/ssh" "xorm.io/builder" + "xorm.io/xorm" ) const ( @@ -358,6 +358,18 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error { sshOpLocker.Lock() defer sshOpLocker.Unlock() + if setting.SSH.RootPath != "" { + // First of ensure that the RootPath is present, and if not make it with 0700 permissions + // This of course doesn't guarantee that this is the right directory for authorized_keys + // but at least if it's supposed to be this directory and it doesn't exist and we're the + // right user it will at least be created properly. + err := os.MkdirAll(setting.SSH.RootPath, 0700) + if err != nil { + log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err) + return err + } + } + fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys") f, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { @@ -645,6 +657,18 @@ func rewriteAllPublicKeys(e Engine) error { sshOpLocker.Lock() defer sshOpLocker.Unlock() + if setting.SSH.RootPath != "" { + // First of ensure that the RootPath is present, and if not make it with 0700 permissions + // This of course doesn't guarantee that this is the right directory for authorized_keys + // but at least if it's supposed to be this directory and it doesn't exist and we're the + // right user it will at least be created properly. + err := os.MkdirAll(setting.SSH.RootPath, 0700) + if err != nil { + log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err) + return err + } + } + fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys") tmpPath := fPath + ".tmp" t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) diff --git a/models/task.go b/models/task.go new file mode 100644 index 0000000000..cb878d387c --- /dev/null +++ b/models/task.go @@ -0,0 +1,240 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "encoding/json" + "fmt" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/builder" +) + +// Task represents a task +type Task struct { + ID int64 + DoerID int64 `xorm:"index"` // operator + Doer *User `xorm:"-"` + OwnerID int64 `xorm:"index"` // repo owner id, when creating, the repoID maybe zero + Owner *User `xorm:"-"` + RepoID int64 `xorm:"index"` + Repo *Repository `xorm:"-"` + Type structs.TaskType + Status structs.TaskStatus `xorm:"index"` + StartTime timeutil.TimeStamp + EndTime timeutil.TimeStamp + PayloadContent string `xorm:"TEXT"` + Errors string `xorm:"TEXT"` // if task failed, saved the error reason + Created timeutil.TimeStamp `xorm:"created"` +} + +// LoadRepo loads repository of the task +func (task *Task) LoadRepo() error { + return task.loadRepo(x) +} + +func (task *Task) loadRepo(e Engine) error { + if task.Repo != nil { + return nil + } + var repo Repository + has, err := e.ID(task.RepoID).Get(&repo) + if err != nil { + return err + } else if !has { + return ErrRepoNotExist{ + ID: task.RepoID, + } + } + task.Repo = &repo + return nil +} + +// LoadDoer loads do user +func (task *Task) LoadDoer() error { + if task.Doer != nil { + return nil + } + + var doer User + has, err := x.ID(task.DoerID).Get(&doer) + if err != nil { + return err + } else if !has { + return ErrUserNotExist{ + UID: task.DoerID, + } + } + task.Doer = &doer + + return nil +} + +// LoadOwner loads owner user +func (task *Task) LoadOwner() error { + if task.Owner != nil { + return nil + } + + var owner User + has, err := x.ID(task.OwnerID).Get(&owner) + if err != nil { + return err + } else if !has { + return ErrUserNotExist{ + UID: task.OwnerID, + } + } + task.Owner = &owner + + return nil +} + +// UpdateCols updates some columns +func (task *Task) UpdateCols(cols ...string) error { + _, err := x.ID(task.ID).Cols(cols...).Update(task) + return err +} + +// MigrateConfig returns task config when migrate repository +func (task *Task) MigrateConfig() (*structs.MigrateRepoOption, error) { + if task.Type == structs.TaskTypeMigrateRepo { + var opts structs.MigrateRepoOption + err := json.Unmarshal([]byte(task.PayloadContent), &opts) + if err != nil { + return nil, err + } + return &opts, nil + } + return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name()) +} + +// ErrTaskDoesNotExist represents a "TaskDoesNotExist" kind of error. +type ErrTaskDoesNotExist struct { + ID int64 + RepoID int64 + Type structs.TaskType +} + +// IsErrTaskDoesNotExist checks if an error is a ErrTaskIsNotExist. +func IsErrTaskDoesNotExist(err error) bool { + _, ok := err.(ErrTaskDoesNotExist) + return ok +} + +func (err ErrTaskDoesNotExist) Error() string { + return fmt.Sprintf("task is not exist [id: %d, repo_id: %d, type: %d]", + err.ID, err.RepoID, err.Type) +} + +// GetMigratingTask returns the migrating task by repo's id +func GetMigratingTask(repoID int64) (*Task, error) { + var task = Task{ + RepoID: repoID, + Type: structs.TaskTypeMigrateRepo, + } + has, err := x.Get(&task) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTaskDoesNotExist{0, repoID, task.Type} + } + return &task, nil +} + +// FindTaskOptions find all tasks +type FindTaskOptions struct { + Status int +} + +// ToConds generates conditions for database operation. +func (opts FindTaskOptions) ToConds() builder.Cond { + var cond = builder.NewCond() + if opts.Status >= 0 { + cond = cond.And(builder.Eq{"status": opts.Status}) + } + return cond +} + +// FindTasks find all tasks +func FindTasks(opts FindTaskOptions) ([]*Task, error) { + var tasks = make([]*Task, 0, 10) + err := x.Where(opts.ToConds()).Find(&tasks) + return tasks, err +} + +func createTask(e Engine, task *Task) error { + _, err := e.Insert(task) + return err +} + +// CreateMigrateTask creates a migrate task +func CreateMigrateTask(doer, u *User, opts base.MigrateOptions) (*Task, error) { + bs, err := json.Marshal(&opts) + if err != nil { + return nil, err + } + + var task = Task{ + DoerID: doer.ID, + OwnerID: u.ID, + Type: structs.TaskTypeMigrateRepo, + Status: structs.TaskStatusQueue, + PayloadContent: string(bs), + } + + if err := createTask(x, &task); err != nil { + return nil, err + } + + repo, err := CreateRepository(doer, u, CreateRepoOptions{ + Name: opts.RepoName, + Description: opts.Description, + OriginalURL: opts.CloneAddr, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: RepositoryBeingMigrated, + }) + if err != nil { + task.EndTime = timeutil.TimeStampNow() + task.Status = structs.TaskStatusFailed + err2 := task.UpdateCols("end_time", "status") + if err2 != nil { + log.Error("UpdateCols Failed: %v", err2.Error()) + } + return nil, err + } + + task.RepoID = repo.ID + if err = task.UpdateCols("repo_id"); err != nil { + return nil, err + } + + return &task, nil +} + +// FinishMigrateTask updates database when migrate task finished +func FinishMigrateTask(task *Task) error { + task.Status = structs.TaskStatusFinished + task.EndTime = timeutil.TimeStampNow() + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + if _, err := sess.ID(task.ID).Cols("status", "end_time").Update(task); err != nil { + return err + } + task.Repo.Status = RepositoryReady + if _, err := sess.ID(task.RepoID).Cols("status").Update(task.Repo); err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/unit_tests.go b/models/unit_tests.go index b53302dad4..eb4da37fe5 100644 --- a/models/unit_tests.go +++ b/models/unit_tests.go @@ -17,11 +17,11 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" "github.com/stretchr/testify/assert" "github.com/unknwon/com" "gopkg.in/testfixtures.v2" "xorm.io/core" + "xorm.io/xorm" ) // NonexistentID an ID that will never exist diff --git a/models/update.go b/models/update.go index c6ea1a845e..5e941c22c4 100644 --- a/models/update.go +++ b/models/update.go @@ -22,6 +22,8 @@ const ( EnvPusherName = "GITEA_PUSHER_NAME" EnvPusherEmail = "GITEA_PUSHER_EMAIL" EnvPusherID = "GITEA_PUSHER_ID" + EnvKeyID = "GITEA_KEY_ID" + EnvIsDeployKey = "GITEA_IS_DEPLOY_KEY" ) // CommitToPushCommit transforms a git.Commit to PushCommit type. diff --git a/models/user.go b/models/user.go index 030e23c383..7aa1e143e8 100644 --- a/models/user.go +++ b/models/user.go @@ -33,7 +33,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "github.com/go-xorm/xorm" "github.com/unknwon/com" "golang.org/x/crypto/argon2" "golang.org/x/crypto/bcrypt" @@ -41,6 +40,7 @@ import ( "golang.org/x/crypto/scrypt" "golang.org/x/crypto/ssh" "xorm.io/builder" + "xorm.io/xorm" ) // UserType defines the user type @@ -994,10 +994,6 @@ func ChangeUserName(u *User, newUserName string) (err error) { return ErrUserAlreadyExist{newUserName} } - if err = ChangeUsernameInPullRequests(u.Name, newUserName); err != nil { - return fmt.Errorf("ChangeUsernameInPullRequests: %v", err) - } - // Do not fail if directory does not exist if err = os.Rename(UserPath(u.Name), UserPath(newUserName)); err != nil && !os.IsNotExist(err) { return fmt.Errorf("Rename user directory: %v", err) @@ -1324,16 +1320,20 @@ func GetUsersByIDs(ids []int64) ([]*User, error) { } // GetUserIDsByNames returns a slice of ids corresponds to names. -func GetUserIDsByNames(names []string) []int64 { +func GetUserIDsByNames(names []string, ignoreNonExistent bool) ([]int64, error) { ids := make([]int64, 0, len(names)) for _, name := range names { u, err := GetUserByName(name) if err != nil { - continue + if ignoreNonExistent { + continue + } else { + return nil, err + } } ids = append(ids, u.ID) } - return ids + return ids, nil } // UserCommit represents a commit with validation of user. diff --git a/models/user_heatmap_test.go b/models/user_heatmap_test.go index a71202d857..f882b35247 100644 --- a/models/user_heatmap_test.go +++ b/models/user_heatmap_test.go @@ -17,7 +17,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { CountResult int JSONResult string }{ - {2, 1, `[{"timestamp":1540080000,"contributions":1}]`}, + {2, 1, `[{"timestamp":1571616000,"contributions":1}]`}, {3, 0, `[]`}, } // Prepare @@ -41,7 +41,7 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { // Get the heatmap and compare heatmap, err := GetUserHeatmapDataByUser(user) assert.NoError(t, err) - assert.Equal(t, len(actions), len(heatmap)) + assert.Equal(t, len(actions), len(heatmap), "invalid action count: did the test data became too old?") assert.Equal(t, tc.CountResult, len(heatmap)) //Test JSON rendering diff --git a/models/webhook.go b/models/webhook.go index f657c18792..6f2162c799 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -833,6 +833,8 @@ func (t *HookTask) deliver() error { return err } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } case http.MethodGet: u, err := url.Parse(t.URL) diff --git a/models/webhook_dingtalk.go b/models/webhook_dingtalk.go index 6a4bdaf06e..1c6c0a83b8 100644 --- a/models/webhook_dingtalk.go +++ b/models/webhook_dingtalk.go @@ -183,22 +183,36 @@ func getDingtalkIssuesPayload(p *api.IssuePayload) (*DingtalkPayload, error) { } func getDingtalkIssueCommentPayload(p *api.IssueCommentPayload) (*DingtalkPayload, error) { - title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title) + title := fmt.Sprintf("#%d: %s", p.Issue.Index, p.Issue.Title) url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)) var content string switch p.Action { case api.HookIssueCommentCreated: - title = "New comment: " + title + if p.IsPull { + title = "New comment on pull request " + title + } else { + title = "New comment on issue " + title + } content = p.Comment.Body case api.HookIssueCommentEdited: - title = "Comment edited: " + title + if p.IsPull { + title = "Comment edited on pull request " + title + } else { + title = "Comment edited on issue " + title + } content = p.Comment.Body case api.HookIssueCommentDeleted: - title = "Comment deleted: " + title + if p.IsPull { + title = "Comment deleted on pull request " + title + } else { + title = "Comment deleted on issue " + title + } url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) content = p.Comment.Body } + title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) + return &DingtalkPayload{ MsgType: "actionCard", ActionCard: dingtalk.ActionCard{ @@ -282,7 +296,7 @@ func getDingtalkPullRequestApprovalPayload(p *api.PullRequestPayload, event Hook } title = fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body + text = p.Review.Content } diff --git a/models/webhook_discord.go b/models/webhook_discord.go index d7a2de0d11..32039edc9d 100644 --- a/models/webhook_discord.go +++ b/models/webhook_discord.go @@ -75,9 +75,14 @@ func color(clr string) int { } var ( - successColor = color("1ac600") - warnColor = color("ffd930") - failedColor = color("ff3232") + greenColor = color("1ac600") + greenColorLight = color("bfe5bf") + yellowColor = color("ffd930") + greyColor = color("4f545c") + purpleColor = color("7289da") + orangeColor = color("eb6420") + orangeColorLight = color("e68d60") + redColor = color("ff3232") ) // SetSecret sets the discord secret @@ -104,7 +109,7 @@ func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordP { Title: title, URL: p.Repo.HTMLURL + "/src/" + refName, - Color: successColor, + Color: greenColor, Author: DiscordEmbedAuthor{ Name: p.Sender.UserName, URL: setting.AppURL + p.Sender.UserName, @@ -127,7 +132,7 @@ func getDiscordDeletePayload(p *api.DeletePayload, meta *DiscordMeta) (*DiscordP { Title: title, URL: p.Repo.HTMLURL + "/src/" + refName, - Color: warnColor, + Color: redColor, Author: DiscordEmbedAuthor{ Name: p.Sender.UserName, URL: setting.AppURL + p.Sender.UserName, @@ -149,7 +154,7 @@ func getDiscordForkPayload(p *api.ForkPayload, meta *DiscordMeta) (*DiscordPaylo { Title: title, URL: p.Repo.HTMLURL, - Color: successColor, + Color: greenColor, Author: DiscordEmbedAuthor{ Name: p.Sender.UserName, URL: setting.AppURL + p.Sender.UserName, @@ -199,7 +204,7 @@ func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPaylo Title: title, Description: text, URL: titleLink, - Color: successColor, + Color: greenColor, Author: DiscordEmbedAuthor{ Name: p.Sender.UserName, URL: setting.AppURL + p.Sender.UserName, @@ -218,48 +223,48 @@ func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPa case api.HookIssueOpened: title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = orangeColor case api.HookIssueClosed: title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - color = failedColor + color = redColor text = p.Issue.Body case api.HookIssueReOpened: title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueEdited: title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueAssigned: title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName, p.Issue.Assignee.UserName, p.Index, p.Issue.Title) text = p.Issue.Body - color = successColor + color = greenColor case api.HookIssueUnassigned: title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueLabelUpdated: title = fmt.Sprintf("[%s] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueLabelCleared: title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueSynchronized: title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueMilestoned: title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueDemilestoned: title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor } return &DiscordPayload{ @@ -282,26 +287,41 @@ func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPa } func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordMeta) (*DiscordPayload, error) { - title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title) + title := fmt.Sprintf("#%d: %s", p.Issue.Index, p.Issue.Title) url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)) content := "" var color int switch p.Action { case api.HookIssueCommentCreated: - title = "New comment: " + title + if p.IsPull { + title = "New comment on pull request " + title + color = greenColorLight + } else { + title = "New comment on issue " + title + color = orangeColorLight + } content = p.Comment.Body - color = successColor case api.HookIssueCommentEdited: - title = "Comment edited: " + title + if p.IsPull { + title = "Comment edited on pull request " + title + } else { + title = "Comment edited on issue " + title + } content = p.Comment.Body - color = warnColor + color = yellowColor case api.HookIssueCommentDeleted: - title = "Comment deleted: " + title + if p.IsPull { + title = "Comment deleted on pull request " + title + } else { + title = "Comment deleted on issue " + title + } url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) content = p.Comment.Body - color = warnColor + color = redColor } + title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) + return &DiscordPayload{ Username: discord.Username, AvatarURL: discord.IconURL, @@ -328,24 +348,24 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) case api.HookIssueOpened: title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = greenColor case api.HookIssueClosed: if p.PullRequest.HasMerged { title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - color = successColor + color = purpleColor } else { title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - color = failedColor + color = redColor } text = p.PullRequest.Body case api.HookIssueReOpened: title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueEdited: title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueAssigned: list := make([]string, len(p.PullRequest.Assignees)) for i, user := range p.PullRequest.Assignees { @@ -355,31 +375,31 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) strings.Join(list, ", "), p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = successColor + color = greenColor case api.HookIssueUnassigned: title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueLabelUpdated: title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueLabelCleared: title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueSynchronized: title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueMilestoned: title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueDemilestoned: title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor } return &DiscordPayload{ @@ -412,8 +432,18 @@ func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *Disco } title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = warnColor + text = p.Review.Content + + switch event { + case HookEventPullRequestApproved: + color = greenColor + case HookEventPullRequestRejected: + color = redColor + case HookEventPullRequestComment: + color = greyColor + default: + color = yellowColor + } } return &DiscordPayload{ @@ -442,10 +472,10 @@ func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (* case api.HookRepoCreated: title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) url = p.Repository.HTMLURL - color = successColor + color = greenColor case api.HookRepoDeleted: title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) - color = warnColor + color = redColor } return &DiscordPayload{ @@ -473,15 +503,15 @@ func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*Discor case api.HookReleasePublished: title = fmt.Sprintf("[%s] Release created", p.Release.TagName) url = p.Release.URL - color = successColor + color = greenColor case api.HookReleaseUpdated: title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) url = p.Release.URL - color = successColor + color = yellowColor case api.HookReleaseDeleted: title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) url = p.Release.URL - color = successColor + color = redColor } return &DiscordPayload{ diff --git a/models/webhook_msteams.go b/models/webhook_msteams.go index bdbcdbc9d3..e8cdcca3ca 100644 --- a/models/webhook_msteams.go +++ b/models/webhook_msteams.go @@ -74,7 +74,7 @@ func getMSTeamsCreatePayload(p *api.CreatePayload) (*MSTeamsPayload, error) { return &MSTeamsPayload{ Type: "MessageCard", Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", successColor), + ThemeColor: fmt.Sprintf("%x", greenColor), Title: title, Summary: title, Sections: []MSTeamsSection{ @@ -117,7 +117,7 @@ func getMSTeamsDeletePayload(p *api.DeletePayload) (*MSTeamsPayload, error) { return &MSTeamsPayload{ Type: "MessageCard", Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", warnColor), + ThemeColor: fmt.Sprintf("%x", yellowColor), Title: title, Summary: title, Sections: []MSTeamsSection{ @@ -159,7 +159,7 @@ func getMSTeamsForkPayload(p *api.ForkPayload) (*MSTeamsPayload, error) { return &MSTeamsPayload{ Type: "MessageCard", Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", successColor), + ThemeColor: fmt.Sprintf("%x", greenColor), Title: title, Summary: title, Sections: []MSTeamsSection{ @@ -228,7 +228,7 @@ func getMSTeamsPushPayload(p *api.PushPayload) (*MSTeamsPayload, error) { return &MSTeamsPayload{ Type: "MessageCard", Context: "https://schema.org/extensions", - ThemeColor: fmt.Sprintf("%x", successColor), + ThemeColor: fmt.Sprintf("%x", greenColor), Title: title, Summary: title, Sections: []MSTeamsSection{ @@ -272,48 +272,48 @@ func getMSTeamsIssuesPayload(p *api.IssuePayload) (*MSTeamsPayload, error) { case api.HookIssueOpened: title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = orangeColor case api.HookIssueClosed: title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) - color = failedColor + color = redColor text = p.Issue.Body case api.HookIssueReOpened: title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueEdited: title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueAssigned: title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName, p.Issue.Assignee.UserName, p.Index, p.Issue.Title) text = p.Issue.Body - color = successColor + color = greenColor case api.HookIssueUnassigned: title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueLabelUpdated: title = fmt.Sprintf("[%s] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueLabelCleared: title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueSynchronized: title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueMilestoned: title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor case api.HookIssueDemilestoned: title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title) text = p.Issue.Body - color = warnColor + color = yellowColor } return &MSTeamsPayload{ @@ -356,26 +356,41 @@ func getMSTeamsIssuesPayload(p *api.IssuePayload) (*MSTeamsPayload, error) { } func getMSTeamsIssueCommentPayload(p *api.IssueCommentPayload) (*MSTeamsPayload, error) { - title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title) + title := fmt.Sprintf("#%d: %s", p.Issue.Index, p.Issue.Title) url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)) content := "" var color int switch p.Action { case api.HookIssueCommentCreated: - title = "New comment: " + title + if p.IsPull { + title = "New comment on pull request " + title + color = greenColorLight + } else { + title = "New comment on issue " + title + color = orangeColorLight + } content = p.Comment.Body - color = successColor case api.HookIssueCommentEdited: - title = "Comment edited: " + title + if p.IsPull { + title = "Comment edited on pull request " + title + } else { + title = "Comment edited on issue " + title + } content = p.Comment.Body - color = warnColor + color = yellowColor case api.HookIssueCommentDeleted: - title = "Comment deleted: " + title + if p.IsPull { + title = "Comment deleted on pull request " + title + } else { + title = "Comment deleted on issue " + title + } url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index) content = p.Comment.Body - color = warnColor + color = redColor } + title = fmt.Sprintf("[%s] %s", p.Repository.FullName, title) + return &MSTeamsPayload{ Type: "MessageCard", Context: "https://schema.org/extensions", @@ -422,24 +437,24 @@ func getMSTeamsPullRequestPayload(p *api.PullRequestPayload) (*MSTeamsPayload, e case api.HookIssueOpened: title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = greenColor case api.HookIssueClosed: if p.PullRequest.HasMerged { title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - color = successColor + color = purpleColor } else { title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) - color = failedColor + color = redColor } text = p.PullRequest.Body case api.HookIssueReOpened: title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueEdited: title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueAssigned: list := make([]string, len(p.PullRequest.Assignees)) for i, user := range p.PullRequest.Assignees { @@ -449,31 +464,31 @@ func getMSTeamsPullRequestPayload(p *api.PullRequestPayload) (*MSTeamsPayload, e strings.Join(list, ", "), p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = successColor + color = greenColor case api.HookIssueUnassigned: title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueLabelUpdated: title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueLabelCleared: title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueSynchronized: title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueMilestoned: title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor case api.HookIssueDemilestoned: title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title) text = p.PullRequest.Body - color = warnColor + color = yellowColor } return &MSTeamsPayload{ @@ -526,8 +541,18 @@ func getMSTeamsPullRequestApprovalPayload(p *api.PullRequestPayload, event HookE } title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title) - text = p.PullRequest.Body - color = warnColor + text = p.Review.Content + + switch event { + case HookEventPullRequestApproved: + color = greenColor + case HookEventPullRequestRejected: + color = redColor + case HookEventPullRequestComment: + color = greyColor + default: + color = yellowColor + } } return &MSTeamsPayload{ @@ -576,10 +601,10 @@ func getMSTeamsRepositoryPayload(p *api.RepositoryPayload) (*MSTeamsPayload, err case api.HookRepoCreated: title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) url = p.Repository.HTMLURL - color = successColor + color = greenColor case api.HookRepoDeleted: title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) - color = warnColor + color = yellowColor } return &MSTeamsPayload{ @@ -623,15 +648,15 @@ func getMSTeamsReleasePayload(p *api.ReleasePayload) (*MSTeamsPayload, error) { case api.HookReleasePublished: title = fmt.Sprintf("[%s] Release created", p.Release.TagName) url = p.Release.URL - color = successColor + color = greenColor case api.HookReleaseUpdated: title = fmt.Sprintf("[%s] Release updated", p.Release.TagName) url = p.Release.URL - color = successColor + color = greenColor case api.HookReleaseDeleted: title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName) url = p.Release.URL - color = successColor + color = greenColor } return &MSTeamsPayload{ diff --git a/models/wiki.go b/models/wiki.go index 0460e0f079..858fe1d6d0 100644 --- a/models/wiki.go +++ b/models/wiki.go @@ -205,6 +205,13 @@ func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, con commitTreeOpts := git.CommitTreeOpts{ Message: message, } + + sign, signingKey := repo.SignWikiCommit(doer) + if sign { + commitTreeOpts.KeyID = signingKey + } else { + commitTreeOpts.NoGPGSign = true + } if hasMasterBranch { commitTreeOpts.Parents = []string{"HEAD"} } @@ -307,11 +314,19 @@ func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error) return err } message := "Delete page '" + wikiName + "'" - - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, git.CommitTreeOpts{ + commitTreeOpts := git.CommitTreeOpts{ Message: message, Parents: []string{"HEAD"}, - }) + } + + sign, signingKey := repo.SignWikiCommit(doer) + if sign { + commitTreeOpts.KeyID = signingKey + } else { + commitTreeOpts.NoGPGSign = true + } + + commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts) if err != nil { return err } diff --git a/modules/auth/auth.go b/modules/auth/auth.go index 624bb15cbf..1ba149f0f8 100644 --- a/modules/auth/auth.go +++ b/modules/auth/auth.go @@ -224,6 +224,9 @@ func SignedInUser(ctx *macaron.Context, sess session.Store) (*models.User, bool) } if u == nil { + if !setting.Service.EnableBasicAuth { + return nil, false + } u, err = models.UserSignIn(uname, passwd) if err != nil { if !models.IsErrUserNotExist(err) { diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index 242254e600..20dfb15e81 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-xorm/xorm" "github.com/lafriks/xormstore" "github.com/markbates/goth" "github.com/markbates/goth/gothic" @@ -26,6 +25,7 @@ import ( "github.com/markbates/goth/providers/openidConnect" "github.com/markbates/goth/providers/twitter" "github.com/satori/go.uuid" + "xorm.io/xorm" ) var ( diff --git a/modules/auth/org.go b/modules/auth/org.go index 9384d3571d..028b05427a 100644 --- a/modules/auth/org.go +++ b/modules/auth/org.go @@ -22,8 +22,9 @@ import ( // CreateOrgForm form for creating organization type CreateOrgForm struct { - OrgName string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"` - Visibility structs.VisibleType + OrgName string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"` + Visibility structs.VisibleType + RepoAdminChangeTeamAccess bool } // Validate validates the fields diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 8d10fc1570..2280666114 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -152,6 +152,7 @@ type ProtectBranchForm struct { EnableWhitelist bool WhitelistUsers string WhitelistTeams string + WhitelistDeployKeys bool EnableMergeWhitelist bool MergeWhitelistUsers string MergeWhitelistTeams string @@ -557,7 +558,7 @@ func (f *NewWikiForm) Validate(ctx *macaron.Context, errs binding.Errors) bindin // EditRepoFileForm form for changing repository file type EditRepoFileForm struct { TreePath string `binding:"Required;MaxSize(500)"` - Content string `binding:"Required"` + Content string CommitSummary string `binding:"MaxSize(100)"` CommitMessage string CommitChoice string `binding:"Required;MaxSize(50)"` diff --git a/modules/charset/charset_test.go b/modules/charset/charset_test.go index 3c77f12789..a81a6e03ee 100644 --- a/modules/charset/charset_test.go +++ b/modules/charset/charset_test.go @@ -179,7 +179,8 @@ func TestToUTF8DropErrors(t *testing.T) { // "Hola, así cómo ños" res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73}) - assert.Equal(t, []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xC3, 0xAD, 0x20, 0x63, 0xC3, 0xB3, 0x6D, 0x6F, 0x20, 0xC3, 0xB1, 0x6F, 0x73}, res) + assert.Equal(t, []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73}, res[:8]) + assert.Equal(t, []byte{0x73}, res[len(res)-1:]) // "Hola, así cómo " minmatch := []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xC3, 0xAD, 0x20, 0x63, 0xC3, 0xB3, 0x6D, 0x6F, 0x20} diff --git a/modules/context/org.go b/modules/context/org.go index 4867474334..10791c9d01 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -63,7 +63,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { // Force redirection when username is actually a user. if !org.IsOrganization() { - ctx.Redirect("/" + org.Name) + ctx.Redirect(setting.AppSubURL + "/" + org.Name) return } diff --git a/modules/context/repo.go b/modules/context/repo.go index 3caf583f83..8a9c9e4b8c 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -19,8 +19,8 @@ import ( "code.gitea.io/gitea/modules/setting" "gitea.com/macaron/macaron" + "github.com/editorconfig/editorconfig-core-go/v2" "github.com/unknwon/com" - "gopkg.in/editorconfig/editorconfig-core-go.v1" ) // PullRequest contains informations to make a pull request @@ -146,6 +146,9 @@ func (r *Repository) FileExists(path string, branch string) (bool, error) { // GetEditorconfig returns the .editorconfig definition if found in the // HEAD of the default repo branch. func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) { + if r.GitRepo == nil { + return nil, nil + } commit, err := r.GitRepo.GetBranchCommit(r.Repository.DefaultBranch) if err != nil { return nil, err @@ -233,7 +236,7 @@ func RedirectToRepo(ctx *Context, redirectRepoID int64) { if ctx.Req.URL.RawQuery != "" { redirectPath += "?" + ctx.Req.URL.RawQuery } - ctx.Redirect(redirectPath) + ctx.Redirect(path.Join(setting.AppSubURL, redirectPath)) } func repoAssignment(ctx *Context, repo *models.Repository) { @@ -358,12 +361,6 @@ func RepoAssignment() macaron.Handler { return } - gitRepo, err := git.OpenRepository(models.RepoPath(userName, repoName)) - if err != nil { - ctx.ServerError("RepoAssignment Invalid repo "+models.RepoPath(userName, repoName), err) - return - } - ctx.Repo.GitRepo = gitRepo ctx.Repo.RepoLink = repo.Link() ctx.Data["RepoLink"] = ctx.Repo.RepoLink ctx.Data["RepoRelPath"] = ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name @@ -373,13 +370,6 @@ func RepoAssignment() macaron.Handler { ctx.Data["RepoExternalIssuesLink"] = unit.ExternalTrackerConfig().ExternalTrackerURL } - tags, err := ctx.Repo.GitRepo.GetTags() - if err != nil { - ctx.ServerError("GetTags", err) - return - } - ctx.Data["Tags"] = tags - count, err := models.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{ IncludeDrafts: false, IncludeTags: true, @@ -424,13 +414,32 @@ func RepoAssignment() macaron.Handler { } } - // repo is empty and display enable + // Disable everything when the repo is being created + if ctx.Repo.Repository.IsBeingCreated() { + ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch + return + } + + gitRepo, err := git.OpenRepository(models.RepoPath(userName, repoName)) + if err != nil { + ctx.ServerError("RepoAssignment Invalid repo "+models.RepoPath(userName, repoName), err) + return + } + ctx.Repo.GitRepo = gitRepo + + // Stop at this point when the repo is empty. if ctx.Repo.Repository.IsEmpty { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch return } - ctx.Data["TagName"] = ctx.Repo.TagName + tags, err := ctx.Repo.GitRepo.GetTags() + if err != nil { + ctx.ServerError("GetTags", err) + return + } + ctx.Data["Tags"] = tags + brs, err := ctx.Repo.GitRepo.GetBranches() if err != nil { ctx.ServerError("GetBranches", err) @@ -439,6 +448,8 @@ func RepoAssignment() macaron.Handler { ctx.Data["Branches"] = brs ctx.Data["BranchesCount"] = len(brs) + ctx.Data["TagName"] = ctx.Repo.TagName + // If not branch selected, try default one. // If default branch doesn't exists, fall back to some other branch. if len(ctx.Repo.BranchName) == 0 { diff --git a/modules/cron/cron.go b/modules/cron/cron.go index 089f0fa767..795fafb51f 100644 --- a/modules/cron/cron.go +++ b/modules/cron/cron.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migrations" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/sync" mirror_service "code.gitea.io/gitea/services/mirror" @@ -18,12 +19,13 @@ import ( ) const ( - mirrorUpdate = "mirror_update" - gitFsck = "git_fsck" - checkRepos = "check_repos" - archiveCleanup = "archive_cleanup" - syncExternalUsers = "sync_external_users" - deletedBranchesCleanup = "deleted_branches_cleanup" + mirrorUpdate = "mirror_update" + gitFsck = "git_fsck" + checkRepos = "check_repos" + archiveCleanup = "archive_cleanup" + syncExternalUsers = "sync_external_users" + deletedBranchesCleanup = "deleted_branches_cleanup" + updateMigrationPosterID = "update_migration_post_id" ) var c = cron.New() @@ -117,6 +119,15 @@ func NewContext() { go WithUnique(deletedBranchesCleanup, models.RemoveOldDeletedBranches)() } } + + entry, err = c.AddFunc("Update migrated repositories' issues and comments' posterid", setting.Cron.UpdateMigrationPosterID.Schedule, WithUnique(updateMigrationPosterID, migrations.UpdateMigrationPosterID)) + if err != nil { + log.Fatal("Cron[Update migrated repositories]: %v", err) + } + entry.Prev = time.Now() + entry.ExecTimes++ + go WithUnique(updateMigrationPosterID, migrations.UpdateMigrationPosterID)() + c.Start() } diff --git a/modules/git/commit.go b/modules/git/commit.go index eb442f988d..45b943e79e 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -498,3 +498,11 @@ func GetFullCommitID(repoPath, shortID string) (string, error) { } return strings.TrimSpace(commitID), nil } + +// GetRepositoryDefaultPublicGPGKey returns the default public key for this commit +func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) { + if c.repo == nil { + return nil, nil + } + return c.repo.GetDefaultPublicGPGKey(forceUpdate) +} diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index d8bf88a47c..e74ddbfb05 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -72,6 +72,7 @@ func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache LastCom treeCommit = commit } else if rev, ok := revs[""]; ok { treeCommit = convertCommit(rev) + treeCommit.repo = commit.repo } return commitsInfo, treeCommit, nil } diff --git a/modules/git/repo.go b/modules/git/repo.go index 1a9112132f..dd886f3a2e 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -32,6 +32,16 @@ type Repository struct { gogitRepo *gogit.Repository gogitStorage *filesystem.Storage + gpgSettings *GPGSettings +} + +// GPGSettings represents the default GPG settings for this repository +type GPGSettings struct { + Sign bool + KeyID string + Email string + Name string + PublicKeyContent string } const prettyLogFormat = `--pretty=format:%H` diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 9209f4a764..a2bf9ac973 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -28,8 +28,14 @@ func IsBranchExist(repoPath, name string) bool { // IsBranchExist returns true if given branch exists in current repository. func (repo *Repository) IsBranchExist(name string) bool { - _, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true) - return err == nil + if name == "" { + return false + } + reference, err := repo.gogitRepo.Reference(plumbing.ReferenceName(BranchPrefix+name), true) + if err != nil { + return false + } + return reference.Type() != plumbing.InvalidReference } // Branch represents a Git branch. @@ -165,7 +171,7 @@ func (repo *Repository) AddRemote(name, url string, fetch bool) error { // RemoveRemote removes a remote from repository. func (repo *Repository) RemoveRemote(name string) error { - _, err := NewCommand("remote", "remove", name).RunInDir(repo.Path) + _, err := NewCommand("remote", "rm", name).RunInDir(repo.Path) return err } diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go new file mode 100644 index 0000000000..b4c3f3b431 --- /dev/null +++ b/modules/git/repo_gpg.go @@ -0,0 +1,59 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "fmt" + "strings" + + "code.gitea.io/gitea/modules/process" +) + +// LoadPublicKeyContent will load the key from gpg +func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { + content, stderr, err := process.GetManager().Exec( + "gpg -a --export", + "gpg", "-a", "--export", gpgSettings.KeyID) + if err != nil { + return fmt.Errorf("Unable to get default signing key: %s, %s, %v", gpgSettings.KeyID, stderr, err) + } + gpgSettings.PublicKeyContent = content + return nil +} + +// GetDefaultPublicGPGKey will return and cache the default public GPG settings for this repository +func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) { + if repo.gpgSettings != nil && !forceUpdate { + return repo.gpgSettings, nil + } + + gpgSettings := &GPGSettings{ + Sign: true, + } + + value, _ := NewCommand("config", "--get", "commit.gpgsign").RunInDir(repo.Path) + sign, valid := ParseBool(strings.TrimSpace(value)) + if !sign || !valid { + gpgSettings.Sign = false + repo.gpgSettings = gpgSettings + return gpgSettings, nil + } + + signingKey, _ := NewCommand("config", "--get", "user.signingkey").RunInDir(repo.Path) + gpgSettings.KeyID = strings.TrimSpace(signingKey) + + defaultEmail, _ := NewCommand("config", "--get", "user.email").RunInDir(repo.Path) + gpgSettings.Email = strings.TrimSpace(defaultEmail) + + defaultName, _ := NewCommand("config", "--get", "user.name").RunInDir(repo.Path) + gpgSettings.Name = strings.TrimSpace(defaultName) + + if err := gpgSettings.LoadPublicKeyContent(); err != nil { + return nil, err + } + repo.gpgSettings = gpgSettings + return repo.gpgSettings, nil +} diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index b31e4330cd..8f91f4efac 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -6,10 +6,13 @@ package git import ( + "bytes" "fmt" "os" "strings" "time" + + "github.com/mcuadros/go-version" ) func (repo *Repository) getTree(id SHA1) (*Tree, error) { @@ -53,14 +56,20 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) { // CommitTreeOpts represents the possible options to CommitTree type CommitTreeOpts struct { - Parents []string - Message string - KeyID string - NoGPGSign bool + Parents []string + Message string + KeyID string + NoGPGSign bool + AlwaysSign bool } // CommitTree creates a commit from a given tree id for the user with provided message func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOpts) (SHA1, error) { + binVersion, err := BinVersion() + if err != nil { + return SHA1{}, err + } + commitTimeStr := time.Now().Format(time.RFC3339) // Because this may call hooks we should pass in the environment @@ -78,20 +87,24 @@ func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOp cmd.AddArguments("-p", parent) } - cmd.AddArguments("-m", opts.Message) + messageBytes := new(bytes.Buffer) + _, _ = messageBytes.WriteString(opts.Message) + _, _ = messageBytes.WriteString("\n") - if opts.KeyID != "" { + if version.Compare(binVersion, "1.7.9", ">=") && (opts.KeyID != "" || opts.AlwaysSign) { cmd.AddArguments(fmt.Sprintf("-S%s", opts.KeyID)) } - if opts.NoGPGSign { + if version.Compare(binVersion, "2.0.0", ">=") && opts.NoGPGSign { cmd.AddArguments("--no-gpg-sign") } - res, err := cmd.RunInDirWithEnv(repo.Path, env) + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + err = cmd.RunInDirTimeoutEnvFullPipeline(env, -1, repo.Path, stdout, stderr, messageBytes) if err != nil { - return SHA1{}, err + return SHA1{}, concatenateError(err, stderr.String()) } - return NewIDFromString(strings.TrimSpace(res)) + return NewIDFromString(strings.TrimSpace(stdout.String())) } diff --git a/modules/git/utils.go b/modules/git/utils.go index 83cd21f34e..e791f16041 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -7,6 +7,7 @@ package git import ( "fmt" "os" + "strconv" "strings" "sync" ) @@ -86,3 +87,30 @@ func RefEndName(refStr string) string { return refStr } + +// ParseBool returns the boolean value represented by the string as per git's git_config_bool +// true will be returned for the result if the string is empty, but valid will be false. +// "true", "yes", "on" are all true, true +// "false", "no", "off" are all false, true +// 0 is false, true +// Any other integer is true, true +// Anything else will return false, false +func ParseBool(value string) (result bool, valid bool) { + // Empty strings are true but invalid + if len(value) == 0 { + return true, false + } + // These are the git expected true and false values + if strings.EqualFold(value, "true") || strings.EqualFold(value, "yes") || strings.EqualFold(value, "on") { + return true, true + } + if strings.EqualFold(value, "false") || strings.EqualFold(value, "no") || strings.EqualFold(value, "off") { + return false, true + } + // Try a number + intValue, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return false, false + } + return intValue != 0, true +} diff --git a/modules/graceful/cleanup.go b/modules/graceful/cleanup.go new file mode 100644 index 0000000000..84355a9a70 --- /dev/null +++ b/modules/graceful/cleanup.go @@ -0,0 +1,40 @@ +// +build !windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package graceful + +import "sync" + +var cleanupWaitGroup sync.WaitGroup + +func init() { + cleanupWaitGroup = sync.WaitGroup{} + + // There are three places that could inherit sockets: + // + // * HTTP or HTTPS main listener + // * HTTP redirection fallback + // * SSH + // + // If you add an additional place you must increment this number + // and add a function to call InformCleanup if it's not going to be used + cleanupWaitGroup.Add(3) + + // Wait till we're done getting all of the listeners and then close + // the unused ones + go func() { + cleanupWaitGroup.Wait() + // Ignore the error here there's not much we can do with it + // They're logged in the CloseProvidedListeners function + _ = CloseProvidedListeners() + }() +} + +// InformCleanup tells the cleanup wait group that we have either taken a listener +// or will not be taking a listener +func InformCleanup() { + cleanupWaitGroup.Done() +} diff --git a/modules/graceful/graceful_windows.go b/modules/graceful/graceful_windows.go new file mode 100644 index 0000000000..281b255fb5 --- /dev/null +++ b/modules/graceful/graceful_windows.go @@ -0,0 +1,16 @@ +// +build windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// This code is heavily inspired by the archived gofacebook/gracenet/net.go handler + +package graceful + +// This file contains shims for windows builds +const IsChild = false + +// WaitForServers waits for all running servers to finish +func WaitForServers() { + +} diff --git a/modules/graceful/net.go b/modules/graceful/net.go new file mode 100644 index 0000000000..af484641c6 --- /dev/null +++ b/modules/graceful/net.go @@ -0,0 +1,211 @@ +// +build !windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// This code is heavily inspired by the archived gofacebook/gracenet/net.go handler + +package graceful + +import ( + "fmt" + "net" + "os" + "strconv" + "strings" + "sync" + + "code.gitea.io/gitea/modules/log" +) + +const ( + listenFDs = "LISTEN_FDS" + startFD = 3 +) + +// In order to keep the working directory the same as when we started we record +// it at startup. +var originalWD, _ = os.Getwd() + +var ( + once = sync.Once{} + mutex = sync.Mutex{} + + providedListeners = []net.Listener{} + activeListeners = []net.Listener{} +) + +func getProvidedFDs() (savedErr error) { + // Only inherit the provided FDS once but we will save the error so that repeated calls to this function will return the same error + once.Do(func() { + mutex.Lock() + defer mutex.Unlock() + + numFDs := os.Getenv(listenFDs) + if numFDs == "" { + return + } + n, err := strconv.Atoi(numFDs) + if err != nil { + savedErr = fmt.Errorf("%s is not a number: %s. Err: %v", listenFDs, numFDs, err) + return + } + + for i := startFD; i < n+startFD; i++ { + file := os.NewFile(uintptr(i), fmt.Sprintf("listener_FD%d", i)) + + l, err := net.FileListener(file) + if err == nil { + // Close the inherited file if it's a listener + if err = file.Close(); err != nil { + savedErr = fmt.Errorf("error closing provided socket fd %d: %s", i, err) + return + } + providedListeners = append(providedListeners, l) + continue + } + + // If needed we can handle packetconns here. + savedErr = fmt.Errorf("Error getting provided socket fd %d: %v", i, err) + return + } + }) + return savedErr +} + +// CloseProvidedListeners closes all unused provided listeners. +func CloseProvidedListeners() error { + mutex.Lock() + defer mutex.Unlock() + var returnableError error + for _, l := range providedListeners { + err := l.Close() + if err != nil { + log.Error("Error in closing unused provided listener: %v", err) + if returnableError != nil { + returnableError = fmt.Errorf("%v & %v", returnableError, err) + } else { + returnableError = err + } + } + } + providedListeners = []net.Listener{} + + return returnableError +} + +// GetListener obtains a listener for the local network address. The network must be +// a stream-oriented network: "tcp", "tcp4", "tcp6", "unix" or "unixpacket". It +// returns an provided net.Listener for the matching network and address, or +// creates a new one using net.Listen. +func GetListener(network, address string) (net.Listener, error) { + // Add a deferral to say that we've tried to grab a listener + defer InformCleanup() + switch network { + case "tcp", "tcp4", "tcp6": + tcpAddr, err := net.ResolveTCPAddr(network, address) + if err != nil { + return nil, err + } + return GetListenerTCP(network, tcpAddr) + case "unix", "unixpacket": + unixAddr, err := net.ResolveUnixAddr(network, address) + if err != nil { + return nil, err + } + return GetListenerUnix(network, unixAddr) + default: + return nil, net.UnknownNetworkError(network) + } +} + +// GetListenerTCP announces on the local network address. The network must be: +// "tcp", "tcp4" or "tcp6". It returns a provided net.Listener for the +// matching network and address, or creates a new one using net.ListenTCP. +func GetListenerTCP(network string, address *net.TCPAddr) (*net.TCPListener, error) { + if err := getProvidedFDs(); err != nil { + return nil, err + } + + mutex.Lock() + defer mutex.Unlock() + + // look for a provided listener + for i, l := range providedListeners { + if isSameAddr(l.Addr(), address) { + providedListeners = append(providedListeners[:i], providedListeners[i+1:]...) + + activeListeners = append(activeListeners, l) + return l.(*net.TCPListener), nil + } + } + + // no provided listener for this address -> make a fresh listener + l, err := net.ListenTCP(network, address) + if err != nil { + return nil, err + } + activeListeners = append(activeListeners, l) + return l, nil +} + +// GetListenerUnix announces on the local network address. The network must be: +// "unix" or "unixpacket". It returns a provided net.Listener for the +// matching network and address, or creates a new one using net.ListenUnix. +func GetListenerUnix(network string, address *net.UnixAddr) (*net.UnixListener, error) { + if err := getProvidedFDs(); err != nil { + return nil, err + } + + mutex.Lock() + defer mutex.Unlock() + + // look for a provided listener + for i, l := range providedListeners { + if isSameAddr(l.Addr(), address) { + providedListeners = append(providedListeners[:i], providedListeners[i+1:]...) + activeListeners = append(activeListeners, l) + return l.(*net.UnixListener), nil + } + } + + // make a fresh listener + l, err := net.ListenUnix(network, address) + if err != nil { + return nil, err + } + activeListeners = append(activeListeners, l) + return l, nil +} + +func isSameAddr(a1, a2 net.Addr) bool { + // If the addresses are not on the same network fail. + if a1.Network() != a2.Network() { + return false + } + + // If the two addresses have the same string representation they're equal + a1s := a1.String() + a2s := a2.String() + if a1s == a2s { + return true + } + + // This allows for ipv6 vs ipv4 local addresses to compare as equal. This + // scenario is common when listening on localhost. + const ipv6prefix = "[::]" + a1s = strings.TrimPrefix(a1s, ipv6prefix) + a2s = strings.TrimPrefix(a2s, ipv6prefix) + const ipv4prefix = "0.0.0.0" + a1s = strings.TrimPrefix(a1s, ipv4prefix) + a2s = strings.TrimPrefix(a2s, ipv4prefix) + return a1s == a2s +} + +func getActiveListeners() []net.Listener { + mutex.Lock() + defer mutex.Unlock() + listeners := make([]net.Listener, len(activeListeners)) + copy(listeners, activeListeners) + return listeners +} diff --git a/modules/graceful/restart.go b/modules/graceful/restart.go new file mode 100644 index 0000000000..04ee072c80 --- /dev/null +++ b/modules/graceful/restart.go @@ -0,0 +1,85 @@ +// +build !windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// This code is heavily inspired by the archived gofacebook/gracenet/net.go handler + +package graceful + +import ( + "fmt" + "os" + "os/exec" + "strings" + "sync" + "syscall" +) + +var killParent sync.Once + +// KillParent sends the kill signal to the parent process if we are a child +func KillParent() { + killParent.Do(func() { + if IsChild { + ppid := syscall.Getppid() + if ppid > 1 { + _ = syscall.Kill(ppid, syscall.SIGTERM) + } + } + }) +} + +// RestartProcess starts a new process passing it the active listeners. It +// doesn't fork, but starts a new process using the same environment and +// arguments as when it was originally started. This allows for a newly +// deployed binary to be started. It returns the pid of the newly started +// process when successful. +func RestartProcess() (int, error) { + listeners := getActiveListeners() + + // Extract the fds from the listeners. + files := make([]*os.File, len(listeners)) + for i, l := range listeners { + var err error + // Now, all our listeners actually have File() functions so instead of + // individually casting we just use a hacky interface + files[i], err = l.(filer).File() + if err != nil { + return 0, err + } + // Remember to close these at the end. + defer files[i].Close() + } + + // Use the original binary location. This works with symlinks such that if + // the file it points to has been changed we will use the updated symlink. + argv0, err := exec.LookPath(os.Args[0]) + if err != nil { + return 0, err + } + + // Pass on the environment and replace the old count key with the new one. + var env []string + for _, v := range os.Environ() { + if !strings.HasPrefix(v, listenFDs+"=") { + env = append(env, v) + } + } + env = append(env, fmt.Sprintf("%s=%d", listenFDs, len(listeners))) + + allFiles := append([]*os.File{os.Stdin, os.Stdout, os.Stderr}, files...) + process, err := os.StartProcess(argv0, os.Args, &os.ProcAttr{ + Dir: originalWD, + Env: env, + Files: allFiles, + }) + if err != nil { + return 0, err + } + return process.Pid, nil +} + +type filer interface { + File() (*os.File, error) +} diff --git a/modules/graceful/server.go b/modules/graceful/server.go new file mode 100644 index 0000000000..896d547b46 --- /dev/null +++ b/modules/graceful/server.go @@ -0,0 +1,274 @@ +// +build !windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// This code is highly inspired by endless go + +package graceful + +import ( + "crypto/tls" + "net" + "os" + "strings" + "sync" + "syscall" + "time" + + "code.gitea.io/gitea/modules/log" +) + +type state uint8 + +const ( + stateInit state = iota + stateRunning + stateShuttingDown + stateTerminate +) + +var ( + // RWMutex for when adding servers or shutting down + runningServerReg sync.RWMutex + runningServerWG sync.WaitGroup + // ensure we only fork once + runningServersForked bool + + // DefaultReadTimeOut default read timeout + DefaultReadTimeOut time.Duration + // DefaultWriteTimeOut default write timeout + DefaultWriteTimeOut time.Duration + // DefaultMaxHeaderBytes default max header bytes + DefaultMaxHeaderBytes int + + // IsChild reports if we are a fork iff LISTEN_FDS is set and our parent PID is not 1 + IsChild = len(os.Getenv(listenFDs)) > 0 && os.Getppid() > 1 +) + +func init() { + runningServerReg = sync.RWMutex{} + runningServerWG = sync.WaitGroup{} + + DefaultMaxHeaderBytes = 0 // use http.DefaultMaxHeaderBytes - which currently is 1 << 20 (1MB) +} + +// ServeFunction represents a listen.Accept loop +type ServeFunction = func(net.Listener) error + +// Server represents our graceful server +type Server struct { + network string + address string + listener net.Listener + PreSignalHooks map[os.Signal][]func() + PostSignalHooks map[os.Signal][]func() + wg sync.WaitGroup + sigChan chan os.Signal + state state + lock *sync.RWMutex + BeforeBegin func(network, address string) + OnShutdown func() +} + +// WaitForServers waits for all running servers to finish +func WaitForServers() { + runningServerWG.Wait() +} + +// NewServer creates a server on network at provided address +func NewServer(network, address string) *Server { + runningServerReg.Lock() + defer runningServerReg.Unlock() + + if IsChild { + log.Info("Restarting new server: %s:%s on PID: %d", network, address, os.Getpid()) + } else { + log.Info("Starting new server: %s:%s on PID: %d", network, address, os.Getpid()) + } + srv := &Server{ + wg: sync.WaitGroup{}, + sigChan: make(chan os.Signal), + PreSignalHooks: map[os.Signal][]func(){}, + PostSignalHooks: map[os.Signal][]func(){}, + state: stateInit, + lock: &sync.RWMutex{}, + network: network, + address: address, + } + + srv.BeforeBegin = func(network, addr string) { + log.Debug("Starting server on %s:%s (PID: %d)", network, addr, syscall.Getpid()) + } + + return srv +} + +// ListenAndServe listens on the provided network address and then calls Serve +// to handle requests on incoming connections. +func (srv *Server) ListenAndServe(serve ServeFunction) error { + go srv.handleSignals() + + l, err := GetListener(srv.network, srv.address) + if err != nil { + log.Error("Unable to GetListener: %v", err) + return err + } + + srv.listener = newWrappedListener(l, srv) + + KillParent() + + srv.BeforeBegin(srv.network, srv.address) + + return srv.Serve(serve) +} + +// ListenAndServeTLS listens on the provided network address and then calls +// Serve to handle requests on incoming TLS connections. +// +// Filenames containing a certificate and matching private key for the server must +// be provided. If the certificate is signed by a certificate authority, the +// certFile should be the concatenation of the server's certificate followed by the +// CA's certificate. +func (srv *Server) ListenAndServeTLS(certFile, keyFile string, serve ServeFunction) error { + config := &tls.Config{} + if config.NextProtos == nil { + config.NextProtos = []string{"http/1.1"} + } + + config.Certificates = make([]tls.Certificate, 1) + var err error + config.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + log.Error("Failed to load https cert file %s for %s:%s: %v", certFile, srv.network, srv.address, err) + return err + } + return srv.ListenAndServeTLSConfig(config, serve) +} + +// ListenAndServeTLSConfig listens on the provided network address and then calls +// Serve to handle requests on incoming TLS connections. +func (srv *Server) ListenAndServeTLSConfig(tlsConfig *tls.Config, serve ServeFunction) error { + go srv.handleSignals() + + l, err := GetListener(srv.network, srv.address) + if err != nil { + log.Error("Unable to get Listener: %v", err) + return err + } + + wl := newWrappedListener(l, srv) + srv.listener = tls.NewListener(wl, tlsConfig) + + KillParent() + srv.BeforeBegin(srv.network, srv.address) + + return srv.Serve(serve) +} + +// Serve accepts incoming HTTP connections on the wrapped listener l, creating a new +// service goroutine for each. The service goroutines read requests and then call +// handler to reply to them. Handler is typically nil, in which case the +// DefaultServeMux is used. +// +// In addition to the standard Serve behaviour each connection is added to a +// sync.Waitgroup so that all outstanding connections can be served before shutting +// down the server. +func (srv *Server) Serve(serve ServeFunction) error { + defer log.Debug("Serve() returning... (PID: %d)", syscall.Getpid()) + srv.setState(stateRunning) + runningServerWG.Add(1) + err := serve(srv.listener) + log.Debug("Waiting for connections to finish... (PID: %d)", syscall.Getpid()) + srv.wg.Wait() + srv.setState(stateTerminate) + runningServerWG.Done() + // use of closed means that the listeners are closed - i.e. we should be shutting down - return nil + if err != nil && strings.Contains(err.Error(), "use of closed") { + return nil + } + return err +} + +func (srv *Server) getState() state { + srv.lock.RLock() + defer srv.lock.RUnlock() + + return srv.state +} + +func (srv *Server) setState(st state) { + srv.lock.Lock() + defer srv.lock.Unlock() + + srv.state = st +} + +type wrappedListener struct { + net.Listener + stopped bool + server *Server +} + +func newWrappedListener(l net.Listener, srv *Server) *wrappedListener { + return &wrappedListener{ + Listener: l, + server: srv, + } +} + +func (wl *wrappedListener) Accept() (net.Conn, error) { + var c net.Conn + // Set keepalive on TCPListeners connections. + if tcl, ok := wl.Listener.(*net.TCPListener); ok { + tc, err := tcl.AcceptTCP() + if err != nil { + return nil, err + } + _ = tc.SetKeepAlive(true) // see http.tcpKeepAliveListener + _ = tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener + c = tc + } else { + var err error + c, err = wl.Listener.Accept() + if err != nil { + return nil, err + } + } + + c = wrappedConn{ + Conn: c, + server: wl.server, + } + + wl.server.wg.Add(1) + return c, nil +} + +func (wl *wrappedListener) Close() error { + if wl.stopped { + return syscall.EINVAL + } + + wl.stopped = true + return wl.Listener.Close() +} + +func (wl *wrappedListener) File() (*os.File, error) { + // returns a dup(2) - FD_CLOEXEC flag *not* set so the listening socket can be passed to child processes + return wl.Listener.(filer).File() +} + +type wrappedConn struct { + net.Conn + server *Server +} + +func (w wrappedConn) Close() error { + err := w.Conn.Close() + if err == nil { + w.server.wg.Done() + } + return err +} diff --git a/modules/graceful/server_hooks.go b/modules/graceful/server_hooks.go new file mode 100644 index 0000000000..b8ca20ddf5 --- /dev/null +++ b/modules/graceful/server_hooks.go @@ -0,0 +1,121 @@ +// +build !windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package graceful + +import ( + "errors" + "fmt" + "os" + "runtime" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +// shutdown closes the listener so that no new connections are accepted +// and starts a goroutine that will hammer (stop all running requests) the server +// after setting.GracefulHammerTime. +func (srv *Server) shutdown() { + // only shutdown if we're running. + if srv.getState() != stateRunning { + return + } + + srv.setState(stateShuttingDown) + if setting.GracefulHammerTime >= 0 { + go srv.hammerTime(setting.GracefulHammerTime) + } + + if srv.OnShutdown != nil { + srv.OnShutdown() + } + err := srv.listener.Close() + if err != nil { + log.Error("PID: %d Listener.Close() error: %v", os.Getpid(), err) + } else { + log.Info("PID: %d Listener (%s) closed.", os.Getpid(), srv.listener.Addr()) + } +} + +// hammerTime forces the server to shutdown in a given timeout - whether it +// finished outstanding requests or not. if Read/WriteTimeout are not set or the +// max header size is very big a connection could hang... +// +// srv.Serve() will not return until all connections are served. this will +// unblock the srv.wg.Wait() in Serve() thus causing ListenAndServe* functions to +// return. +func (srv *Server) hammerTime(d time.Duration) { + defer func() { + // We call srv.wg.Done() until it panics. + // This happens if we call Done() when the WaitGroup counter is already at 0 + // So if it panics -> we're done, Serve() will return and the + // parent will goroutine will exit. + if r := recover(); r != nil { + log.Error("WaitGroup at 0: Error: %v", r) + } + }() + if srv.getState() != stateShuttingDown { + return + } + time.Sleep(d) + log.Warn("Forcefully shutting down parent") + for { + if srv.getState() == stateTerminate { + break + } + srv.wg.Done() + + // Give other goroutines a chance to finish before we forcibly stop them. + runtime.Gosched() + } +} + +func (srv *Server) fork() error { + runningServerReg.Lock() + defer runningServerReg.Unlock() + + // only one server instance should fork! + if runningServersForked { + return errors.New("another process already forked. Ignoring this one") + } + + runningServersForked = true + + // We need to move the file logs to append pids + setting.RestartLogsWithPIDSuffix() + + _, err := RestartProcess() + + return err +} + +// RegisterPreSignalHook registers a function to be run before the signal handler for +// a given signal. These are not mutex locked and should therefore be only called before Serve. +func (srv *Server) RegisterPreSignalHook(sig os.Signal, f func()) (err error) { + for _, s := range hookableSignals { + if s == sig { + srv.PreSignalHooks[sig] = append(srv.PreSignalHooks[sig], f) + return + } + } + err = fmt.Errorf("Signal %v is not supported", sig) + return +} + +// RegisterPostSignalHook registers a function to be run after the signal handler for +// a given signal. These are not mutex locked and should therefore be only called before Serve. +func (srv *Server) RegisterPostSignalHook(sig os.Signal, f func()) (err error) { + for _, s := range hookableSignals { + if s == sig { + srv.PostSignalHooks[sig] = append(srv.PostSignalHooks[sig], f) + return + } + } + err = fmt.Errorf("Signal %v is not supported", sig) + return +} diff --git a/modules/graceful/server_http.go b/modules/graceful/server_http.go new file mode 100644 index 0000000000..446f0f5551 --- /dev/null +++ b/modules/graceful/server_http.go @@ -0,0 +1,47 @@ +// +build !windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package graceful + +import ( + "crypto/tls" + "net/http" +) + +func newHTTPServer(network, address string, handler http.Handler) (*Server, ServeFunction) { + server := NewServer(network, address) + httpServer := http.Server{ + ReadTimeout: DefaultReadTimeOut, + WriteTimeout: DefaultWriteTimeOut, + MaxHeaderBytes: DefaultMaxHeaderBytes, + Handler: handler, + } + server.OnShutdown = func() { + httpServer.SetKeepAlivesEnabled(false) + } + return server, httpServer.Serve +} + +// HTTPListenAndServe listens on the provided network address and then calls Serve +// to handle requests on incoming connections. +func HTTPListenAndServe(network, address string, handler http.Handler) error { + server, lHandler := newHTTPServer(network, address, handler) + return server.ListenAndServe(lHandler) +} + +// HTTPListenAndServeTLS listens on the provided network address and then calls Serve +// to handle requests on incoming connections. +func HTTPListenAndServeTLS(network, address, certFile, keyFile string, handler http.Handler) error { + server, lHandler := newHTTPServer(network, address, handler) + return server.ListenAndServeTLS(certFile, keyFile, lHandler) +} + +// HTTPListenAndServeTLSConfig listens on the provided network address and then calls Serve +// to handle requests on incoming connections. +func HTTPListenAndServeTLSConfig(network, address string, tlsConfig *tls.Config, handler http.Handler) error { + server, lHandler := newHTTPServer(network, address, handler) + return server.ListenAndServeTLSConfig(tlsConfig, lHandler) +} diff --git a/modules/graceful/server_signals.go b/modules/graceful/server_signals.go new file mode 100644 index 0000000000..a4bcd00b16 --- /dev/null +++ b/modules/graceful/server_signals.go @@ -0,0 +1,95 @@ +// +build !windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package graceful + +import ( + "os" + "os/signal" + "syscall" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +var hookableSignals []os.Signal + +func init() { + hookableSignals = []os.Signal{ + syscall.SIGHUP, + syscall.SIGUSR1, + syscall.SIGUSR2, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGTSTP, + } +} + +// handleSignals listens for os Signals and calls any hooked in function that the +// user had registered with the signal. +func (srv *Server) handleSignals() { + var sig os.Signal + + signal.Notify( + srv.sigChan, + hookableSignals..., + ) + + pid := syscall.Getpid() + for { + sig = <-srv.sigChan + srv.preSignalHooks(sig) + switch sig { + case syscall.SIGHUP: + if setting.GracefulRestartable { + log.Info("PID: %d. Received SIGHUP. Forking...", pid) + err := srv.fork() + if err != nil && err.Error() != "another process already forked. Ignoring this one" { + log.Error("Error whilst forking from PID: %d : %v", pid, err) + } + } else { + log.Info("PID: %d. Received SIGHUP. Not set restartable. Shutting down...", pid) + + srv.shutdown() + } + case syscall.SIGUSR1: + log.Info("PID %d. Received SIGUSR1.", pid) + case syscall.SIGUSR2: + log.Warn("PID %d. Received SIGUSR2. Hammering...", pid) + srv.hammerTime(0 * time.Second) + case syscall.SIGINT: + log.Warn("PID %d. Received SIGINT. Shutting down...", pid) + srv.shutdown() + case syscall.SIGTERM: + log.Warn("PID %d. Received SIGTERM. Shutting down...", pid) + srv.shutdown() + case syscall.SIGTSTP: + log.Info("PID %d. Received SIGTSTP.") + default: + log.Info("PID %d. Received %v.", sig) + } + srv.postSignalHooks(sig) + } +} + +func (srv *Server) preSignalHooks(sig os.Signal) { + if _, notSet := srv.PreSignalHooks[sig]; !notSet { + return + } + for _, f := range srv.PreSignalHooks[sig] { + f() + } +} + +func (srv *Server) postSignalHooks(sig os.Signal) { + if _, notSet := srv.PostSignalHooks[sig]; !notSet { + return + } + for _, f := range srv.PostSignalHooks[sig] { + f() + } +} diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index df8bfd6305..4f410daf4c 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -5,9 +5,11 @@ package issues import ( - "fmt" + "sync" + "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -45,78 +47,143 @@ type Indexer interface { Search(kw string, repoID int64, limit, start int) (*SearchResult, error) } +type indexerHolder struct { + indexer Indexer + mutex sync.RWMutex + cond *sync.Cond +} + +func newIndexerHolder() *indexerHolder { + h := &indexerHolder{} + h.cond = sync.NewCond(h.mutex.RLocker()) + return h +} + +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.cond.Wait() + } + return h.indexer +} + var ( + issueIndexerChannel = make(chan *IndexerData, setting.Indexer.UpdateQueueLength) // issueIndexerQueue queue of issue ids to be updated issueIndexerQueue Queue - issueIndexer Indexer + holder = newIndexerHolder() ) // InitIssueIndexer initialize issue indexer, syncReindex is true then reindex until // all issue index done. -func InitIssueIndexer(syncReindex bool) error { - var populate bool - var dummyQueue bool - switch setting.Indexer.IssueType { - case "bleve": - issueIndexer = NewBleveIndexer(setting.Indexer.IssuePath) - exist, err := issueIndexer.Init() - if err != nil { - return err - } - populate = !exist - case "db": - issueIndexer = &DBIndexer{} - dummyQueue = true - default: - return fmt.Errorf("unknow issue indexer type: %s", setting.Indexer.IssueType) - } - - if dummyQueue { - issueIndexerQueue = &DummyQueue{} - return nil - } - - var err error - switch setting.Indexer.IssueQueueType { - case setting.LevelQueueType: - issueIndexerQueue, err = NewLevelQueue( - issueIndexer, - setting.Indexer.IssueQueueDir, - setting.Indexer.IssueQueueBatchNumber) - if err != nil { - return err - } - case setting.ChannelQueueType: - issueIndexerQueue = NewChannelQueue(issueIndexer, setting.Indexer.IssueQueueBatchNumber) - case setting.RedisQueueType: - addrs, pass, idx, err := parseConnStr(setting.Indexer.IssueQueueConnStr) - if err != nil { - return err - } - issueIndexerQueue, err = NewRedisQueue(addrs, pass, idx, issueIndexer, setting.Indexer.IssueQueueBatchNumber) - if err != nil { - return err - } - default: - return fmt.Errorf("Unsupported indexer queue type: %v", setting.Indexer.IssueQueueType) - } - +func InitIssueIndexer(syncReindex bool) { + waitChannel := make(chan time.Duration) go func() { - err = issueIndexerQueue.Run() - if err != nil { - log.Error("issueIndexerQueue.Run: %v", err) + start := time.Now() + log.Info("Initializing Issue Indexer") + var populate bool + var dummyQueue bool + switch setting.Indexer.IssueType { + case "bleve": + issueIndexer := NewBleveIndexer(setting.Indexer.IssuePath) + exist, err := issueIndexer.Init() + if err != nil { + log.Fatal("Unable to initialize Bleve Issue Indexer: %v", err) + } + populate = !exist + holder.set(issueIndexer) + case "db": + issueIndexer := &DBIndexer{} + holder.set(issueIndexer) + dummyQueue = true + default: + log.Fatal("Unknown issue indexer type: %s", setting.Indexer.IssueType) } - }() - if populate { - if syncReindex { - populateIssueIndexer() + if dummyQueue { + issueIndexerQueue = &DummyQueue{} } else { - go populateIssueIndexer() - } - } + var err error + switch setting.Indexer.IssueQueueType { + case setting.LevelQueueType: + issueIndexerQueue, err = NewLevelQueue( + holder.get(), + setting.Indexer.IssueQueueDir, + setting.Indexer.IssueQueueBatchNumber) + if err != nil { + log.Fatal( + "Unable create level queue for issue queue dir: %s batch number: %d : %v", + setting.Indexer.IssueQueueDir, + setting.Indexer.IssueQueueBatchNumber, + err) + } + case setting.ChannelQueueType: + issueIndexerQueue = NewChannelQueue(holder.get(), setting.Indexer.IssueQueueBatchNumber) + case setting.RedisQueueType: + addrs, pass, idx, err := parseConnStr(setting.Indexer.IssueQueueConnStr) + if err != nil { + log.Fatal("Unable to parse connection string for RedisQueueType: %s : %v", + setting.Indexer.IssueQueueConnStr, + err) + } + issueIndexerQueue, err = NewRedisQueue(addrs, pass, idx, holder.get(), setting.Indexer.IssueQueueBatchNumber) + if err != nil { + log.Fatal("Unable to create RedisQueue: %s : %v", + setting.Indexer.IssueQueueConnStr, + err) + } + default: + log.Fatal("Unsupported indexer queue type: %v", + setting.Indexer.IssueQueueType) + } - return nil + go func() { + err = issueIndexerQueue.Run() + if err != nil { + log.Error("issueIndexerQueue.Run: %v", err) + } + }() + } + + go func() { + for data := range issueIndexerChannel { + _ = issueIndexerQueue.Push(data) + } + }() + + if populate { + if syncReindex { + populateIssueIndexer() + } else { + go populateIssueIndexer() + } + } + waitChannel <- time.Since(start) + }() + if syncReindex { + <-waitChannel + } else if setting.Indexer.StartupTimeout > 0 { + go func() { + timeout := setting.Indexer.StartupTimeout + if graceful.IsChild && setting.GracefulHammerTime > 0 { + timeout += setting.GracefulHammerTime + } + select { + case duration := <-waitChannel: + log.Info("Issue Indexer Initialization took %v", duration) + case <-time.After(timeout): + log.Fatal("Issue Indexer Initialization timed-out after: %v", timeout) + } + }() + } } // populateIssueIndexer populate the issue indexer with issue data @@ -166,13 +233,13 @@ func UpdateIssueIndexer(issue *models.Issue) { comments = append(comments, comment.Content) } } - _ = issueIndexerQueue.Push(&IndexerData{ + issueIndexerChannel <- &IndexerData{ ID: issue.ID, RepoID: issue.RepoID, Title: issue.Title, Content: issue.Content, Comments: comments, - }) + } } // DeleteRepoIssueIndexer deletes repo's all issues indexes @@ -188,16 +255,16 @@ func DeleteRepoIssueIndexer(repo *models.Repository) { return } - _ = issueIndexerQueue.Push(&IndexerData{ + issueIndexerChannel <- &IndexerData{ IDs: ids, IsDelete: true, - }) + } } // SearchIssuesByKeyword search issue ids by keywords and repo id func SearchIssuesByKeyword(repoID int64, keyword string) ([]int64, error) { var issueIDs []int64 - res, err := issueIndexer.Search(keyword, repoID, 1000, 0) + res, err := holder.get().Search(keyword, repoID, 1000, 0) if err != nil { return nil, err } diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 59a7beed47..212c2edfbe 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -5,7 +5,6 @@ package issues import ( - "fmt" "os" "path/filepath" "testing" @@ -17,11 +16,6 @@ import ( "github.com/stretchr/testify/assert" ) -func fatalTestError(fmtStr string, args ...interface{}) { - fmt.Fprintf(os.Stderr, fmtStr, args...) - os.Exit(1) -} - func TestMain(m *testing.M) { models.MainTest(m, filepath.Join("..", "..", "..")) } @@ -32,9 +26,7 @@ func TestBleveSearchIssues(t *testing.T) { os.RemoveAll(setting.Indexer.IssueQueueDir) os.RemoveAll(setting.Indexer.IssuePath) setting.Indexer.IssueType = "bleve" - if err := InitIssueIndexer(true); err != nil { - fatalTestError("Error InitIssueIndexer: %v\n", err) - } + InitIssueIndexer(true) time.Sleep(5 * time.Second) @@ -59,9 +51,7 @@ func TestDBSearchIssues(t *testing.T) { assert.NoError(t, models.PrepareTestDatabase()) setting.Indexer.IssueType = "db" - if err := InitIssueIndexer(true); err != nil { - fatalTestError("Error InitIssueIndexer: %v\n", err) - } + InitIssueIndexer(true) ids, err := SearchIssuesByKeyword(1, "issue2") assert.NoError(t, err) diff --git a/modules/indexer/repo.go b/modules/indexer/repo.go index 91ed173aa7..841f29acd7 100644 --- a/modules/indexer/repo.go +++ b/modules/indexer/repo.go @@ -6,6 +6,7 @@ package indexer import ( "strings" + "sync" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -25,8 +26,36 @@ const ( repoIndexerLatestVersion = 4 ) +type bleveIndexerHolder struct { + index bleve.Index + mutex sync.RWMutex + cond *sync.Cond +} + +func newBleveIndexerHolder() *bleveIndexerHolder { + b := &bleveIndexerHolder{} + b.cond = sync.NewCond(b.mutex.RLocker()) + return b +} + +func (r *bleveIndexerHolder) set(index bleve.Index) { + r.mutex.Lock() + defer r.mutex.Unlock() + r.index = index + r.cond.Broadcast() +} + +func (r *bleveIndexerHolder) get() bleve.Index { + r.mutex.RLock() + defer r.mutex.RUnlock() + if r.index == nil { + r.cond.Wait() + } + return r.index +} + // repoIndexer (thread-safe) index for repository contents -var repoIndexer bleve.Index +var indexerHolder = newBleveIndexerHolder() // RepoIndexerOp type of operation to perform on repo indexer type RepoIndexerOp int @@ -73,12 +102,12 @@ func (update RepoIndexerUpdate) AddToFlushingBatch(batch rupture.FlushingBatch) // InitRepoIndexer initialize repo indexer func InitRepoIndexer(populateIndexer func() error) { - var err error - repoIndexer, err = openIndexer(setting.Indexer.RepoPath, repoIndexerLatestVersion) + indexer, err := openIndexer(setting.Indexer.RepoPath, repoIndexerLatestVersion) if err != nil { log.Fatal("InitRepoIndexer: %v", err) } - if repoIndexer != nil { + if indexer != nil { + indexerHolder.set(indexer) return } @@ -92,7 +121,6 @@ func InitRepoIndexer(populateIndexer func() error) { // createRepoIndexer create a repo indexer if one does not already exist func createRepoIndexer(path string, latestVersion int) error { - var err error docMapping := bleve.NewDocumentMapping() numericFieldMapping := bleve.NewNumericFieldMapping() numericFieldMapping.IncludeInAll = false @@ -103,9 +131,9 @@ func createRepoIndexer(path string, latestVersion int) error { docMapping.AddFieldMappingsAt("Content", textFieldMapping) mapping := bleve.NewIndexMapping() - if err = addUnicodeNormalizeTokenFilter(mapping); err != nil { + if err := addUnicodeNormalizeTokenFilter(mapping); err != nil { return err - } else if err = mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]interface{}{ + } else if err := mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]interface{}{ "type": custom.Name, "char_filters": []string{}, "tokenizer": unicode.Name, @@ -117,10 +145,12 @@ func createRepoIndexer(path string, latestVersion int) error { mapping.AddDocumentMapping(repoIndexerDocType, docMapping) mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping()) - repoIndexer, err = bleve.New(path, mapping) + indexer, err := bleve.New(path, mapping) if err != nil { return err } + indexerHolder.set(indexer) + return rupture.WriteIndexMetadata(path, &rupture.IndexMetadata{ Version: latestVersion, }) @@ -140,14 +170,14 @@ func filenameOfIndexerID(indexerID string) string { // RepoIndexerBatch batch to add updates to func RepoIndexerBatch() rupture.FlushingBatch { - return rupture.NewFlushingBatch(repoIndexer, maxBatchSize) + return rupture.NewFlushingBatch(indexerHolder.get(), maxBatchSize) } // DeleteRepoFromIndexer delete all of a repo's files from indexer func DeleteRepoFromIndexer(repoID int64) error { query := numericEqualityQuery(repoID, "RepoID") searchRequest := bleve.NewSearchRequestOptions(query, 2147483647, 0, false) - result, err := repoIndexer.Search(searchRequest) + result, err := indexerHolder.get().Search(searchRequest) if err != nil { return err } @@ -196,7 +226,7 @@ func SearchRepoByKeyword(repoIDs []int64, keyword string, page, pageSize int) (i searchRequest.Fields = []string{"Content", "RepoID"} searchRequest.IncludeLocations = true - result, err := repoIndexer.Search(searchRequest) + result, err := indexerHolder.get().Search(searchRequest) if err != nil { return 0, nil, err } diff --git a/modules/lfs/locks.go b/modules/lfs/locks.go index d7b2429698..9ffe6b9d59 100644 --- a/modules/lfs/locks.go +++ b/modules/lfs/locks.go @@ -155,7 +155,9 @@ func PostLockHandler(ctx *context.Context) { } var req api.LFSLockRequest - dec := json.NewDecoder(ctx.Req.Body().ReadCloser()) + bodyReader := ctx.Req.Body().ReadCloser() + defer bodyReader.Close() + dec := json.NewDecoder(bodyReader) if err := dec.Decode(&req); err != nil { writeStatus(ctx, 400) return @@ -269,7 +271,9 @@ func UnLockHandler(ctx *context.Context) { } var req api.LFSLockDeleteRequest - dec := json.NewDecoder(ctx.Req.Body().ReadCloser()) + bodyReader := ctx.Req.Body().ReadCloser() + defer bodyReader.Close() + dec := json.NewDecoder(bodyReader) if err := dec.Decode(&req); err != nil { writeStatus(ctx, 400) return diff --git a/modules/lfs/server.go b/modules/lfs/server.go index 652610acf4..6fa97a2894 100644 --- a/modules/lfs/server.go +++ b/modules/lfs/server.go @@ -327,7 +327,9 @@ func PutHandler(ctx *context.Context) { } contentStore := &ContentStore{BasePath: setting.LFS.ContentPath} - if err := contentStore.Put(meta, ctx.Req.Body().ReadCloser()); err != nil { + bodyReader := ctx.Req.Body().ReadCloser() + defer bodyReader.Close() + if err := contentStore.Put(meta, bodyReader); err != nil { ctx.Resp.WriteHeader(500) fmt.Fprintf(ctx.Resp, `{"message":"%s"}`, err) if err = repository.RemoveLFSMetaObjectByOid(rv.Oid); err != nil { @@ -434,7 +436,9 @@ func unpack(ctx *context.Context) *RequestVars { if r.Method == "POST" { // Maybe also check if +json var p RequestVars - dec := json.NewDecoder(r.Body().ReadCloser()) + bodyReader := r.Body().ReadCloser() + defer bodyReader.Close() + dec := json.NewDecoder(bodyReader) err := dec.Decode(&p) if err != nil { return rv @@ -453,7 +457,9 @@ func unpackbatch(ctx *context.Context) *BatchVars { r := ctx.Req var bv BatchVars - dec := json.NewDecoder(r.Body().ReadCloser()) + bodyReader := r.Body().ReadCloser() + defer bodyReader.Close() + dec := json.NewDecoder(bodyReader) err := dec.Decode(&bv) if err != nil { return &bv diff --git a/modules/markup/html.go b/modules/markup/html.go index f07993bc4c..1ff7a41cbb 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -36,17 +37,6 @@ var ( // While fast, this is also incorrect and lead to false positives. // TODO: fix invalid linking issue - // mentionPattern matches all mentions in the form of "@user" - mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_\.]+)(?:\s|$|\)|\])`) - - // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 - issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`) - // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 - issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`) - // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository - // e.g. gogits/gogs#12345 - crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+#[0-9]+)(?:\s|$|\)|\]|\.(\s|$))`) - // sha1CurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae // Although SHA1 hashes are 40 chars long, the regex matches the hash from 7 to 40 chars in length // so that abbreviated hash links can be used as well. This matches git and github useability. @@ -70,6 +60,9 @@ var ( linkRegex, _ = xurls.StrictMatchingScheme("https?://") ) +// CSS class for action keywords (e.g. "closes: #1") +const keywordClass = "issue-keyword" + // regexp for full links to issues/pulls var issueFullPattern *regexp.Regexp @@ -99,15 +92,30 @@ func getIssueFullPattern() *regexp.Regexp { return issueFullPattern } -// FindAllMentions matches mention patterns in given content -// and returns a list of found user names without @ prefix. -func FindAllMentions(content string) []string { - mentions := mentionPattern.FindAllStringSubmatch(content, -1) - ret := make([]string, len(mentions)) - for i, val := range mentions { - ret[i] = val[1][1:] +// CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text +func CustomLinkURLSchemes(schemes []string) { + schemes = append(schemes, "http", "https") + withAuth := make([]string, 0, len(schemes)) + validScheme := regexp.MustCompile(`^[a-z]+$`) + for _, s := range schemes { + if !validScheme.MatchString(s) { + continue + } + without := false + for _, sna := range xurls.SchemesNoAuthority { + if s == sna { + without = true + break + } + } + if without { + s += ":" + } else { + s += "://" + } + withAuth = append(withAuth, s) } - return ret + linkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|")) } // IsSameDomain checks if given url string has the same hostname as current Gitea instance @@ -142,7 +150,6 @@ var defaultProcessors = []processor{ linkProcessor, mentionProcessor, issueIndexPatternProcessor, - crossReferenceIssueIndexPatternProcessor, sha1CurrentPatternProcessor, emailAddressProcessor, } @@ -183,7 +190,6 @@ var commitMessageProcessors = []processor{ linkProcessor, mentionProcessor, issueIndexPatternProcessor, - crossReferenceIssueIndexPatternProcessor, sha1CurrentPatternProcessor, emailAddressProcessor, } @@ -217,7 +223,6 @@ var commitMessageSubjectProcessors = []processor{ linkProcessor, mentionProcessor, issueIndexPatternProcessor, - crossReferenceIssueIndexPatternProcessor, sha1CurrentPatternProcessor, } @@ -330,6 +335,24 @@ func (ctx *postProcessCtx) textNode(node *html.Node) { } } +// createKeyword() renders a highlighted version of an action keyword +func createKeyword(content string) *html.Node { + span := &html.Node{ + Type: html.ElementNode, + Data: atom.Span.String(), + Attr: []html.Attribute{}, + } + span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: keywordClass}) + + text := &html.Node{ + Type: html.TextNode, + Data: content, + } + span.AppendChild(text) + + return span +} + func createLink(href, content, class string) *html.Node { a := &html.Node{ Type: html.ElementNode, @@ -377,10 +400,16 @@ func createCodeLink(href, content, class string) *html.Node { return a } -// replaceContent takes a text node, and in its content it replaces a section of -// it with the specified newNode. An example to visualize how this can work can -// be found here: https://play.golang.org/p/5zP8NnHZ03s +// replaceContent takes text node, and in its content it replaces a section of +// it with the specified newNode. func replaceContent(node *html.Node, i, j int, newNode *html.Node) { + replaceContentList(node, i, j, []*html.Node{newNode}) +} + +// replaceContentList takes text node, and in its content it replaces a section of +// it with the specified newNodes. An example to visualize how this can work can +// be found here: https://play.golang.org/p/5zP8NnHZ03s +func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) { // get the data before and after the match before := node.Data[:i] after := node.Data[j:] @@ -392,7 +421,9 @@ func replaceContent(node *html.Node, i, j int, newNode *html.Node) { // Get the current next sibling, before which we place the replaced data, // and after that we place the new text node. nextSibling := node.NextSibling - node.Parent.InsertBefore(newNode, nextSibling) + for _, n := range newNodes { + node.Parent.InsertBefore(n, nextSibling) + } if after != "" { node.Parent.InsertBefore(&html.Node{ Type: html.TextNode, @@ -402,13 +433,13 @@ func replaceContent(node *html.Node, i, j int, newNode *html.Node) { } func mentionProcessor(_ *postProcessCtx, node *html.Node) { - m := mentionPattern.FindStringSubmatchIndex(node.Data) - if m == nil { + // We replace only the first mention; other mentions will be addressed later + found, loc := references.FindFirstMentionBytes([]byte(node.Data)) + if !found { return } - // Replace the mention with a link to the specified user. - mention := node.Data[m[2]:m[3]] - replaceContent(node, m[2], m[3], createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention")) + mention := node.Data[loc.Start:loc.End] + replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention")) } func shortLinkProcessor(ctx *postProcessCtx, node *html.Node) { @@ -597,45 +628,44 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { if ctx.metas == nil { return } - // default to numeric pattern, unless alphanumeric is requested. - pattern := issueNumericPattern + + var ( + found bool + ref *references.RenderizableReference + ) + if ctx.metas["style"] == IssueNameStyleAlphanumeric { - pattern = issueAlphanumericPattern - } - - match := pattern.FindStringSubmatchIndex(node.Data) - if match == nil { - return - } - - id := node.Data[match[2]:match[3]] - var link *html.Node - if _, ok := ctx.metas["format"]; ok { - // Support for external issue tracker - if ctx.metas["style"] == IssueNameStyleAlphanumeric { - ctx.metas["index"] = id - } else { - ctx.metas["index"] = id[1:] - } - link = createLink(com.Expand(ctx.metas["format"], ctx.metas), id, "issue") + found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data) } else { - link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", id[1:]), id, "issue") + found, ref = references.FindRenderizableReferenceNumeric(node.Data) } - replaceContent(node, match[2], match[3], link) -} - -func crossReferenceIssueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { - m := crossReferenceIssueNumericPattern.FindStringSubmatchIndex(node.Data) - if m == nil { + if !found { return } - ref := node.Data[m[2]:m[3]] - parts := strings.SplitN(ref, "#", 2) - repo, issue := parts[0], parts[1] + var link *html.Node + reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] + if _, ok := ctx.metas["format"]; ok { + ctx.metas["index"] = ref.Issue + link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "issue") + } else if ref.Owner == "" { + link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "issues", ref.Issue), reftext, "issue") + } else { + link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, "issues", ref.Issue), reftext, "issue") + } - replaceContent(node, m[2], m[3], - createLink(util.URLJoin(setting.AppURL, repo, "issues", issue), ref, issue)) + if ref.Action == references.XRefActionNone { + replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) + return + } + + // Decorate action keywords + keyword := createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End]) + spaces := &html.Node{ + Type: html.TextNode, + Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start], + } + replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link}) } // fullSha1PatternProcessor renders SHA containing URLs diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 2824ce3e68..9722063e17 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -239,34 +239,6 @@ func TestRender_FullIssueURLs(t *testing.T) { `#4`) } -func TestRegExp_issueNumericPattern(t *testing.T) { - trueTestCases := []string{ - "#1234", - "#0", - "#1234567890987654321", - " #12", - "#12:", - "ref: #12: msg", - } - falseTestCases := []string{ - "# 1234", - "# 0", - "# ", - "#", - "#ABC", - "#1A2B", - "", - "ABC", - } - - for _, testCase := range trueTestCases { - assert.True(t, issueNumericPattern.MatchString(testCase)) - } - for _, testCase := range falseTestCases { - assert.False(t, issueNumericPattern.MatchString(testCase)) - } -} - func TestRegExp_sha1CurrentPattern(t *testing.T) { trueTestCases := []string{ "d8a994ef243349f321568f9e36d5c3f444b99cae", @@ -325,70 +297,6 @@ func TestRegExp_anySHA1Pattern(t *testing.T) { } } -func TestRegExp_mentionPattern(t *testing.T) { - trueTestCases := []string{ - "@Unknwon", - "@ANT_123", - "@xxx-DiN0-z-A..uru..s-xxx", - " @lol ", - " @Te-st", - "(@gitea)", - "[@gitea]", - } - falseTestCases := []string{ - "@ 0", - "@ ", - "@", - "", - "ABC", - "/home/gitea/@gitea", - "\"@gitea\"", - } - - for _, testCase := range trueTestCases { - res := mentionPattern.MatchString(testCase) - assert.True(t, res) - } - for _, testCase := range falseTestCases { - res := mentionPattern.MatchString(testCase) - assert.False(t, res) - } -} - -func TestRegExp_issueAlphanumericPattern(t *testing.T) { - trueTestCases := []string{ - "ABC-1234", - "A-1", - "RC-80", - "ABCDEFGHIJ-1234567890987654321234567890", - "ABC-123.", - "(ABC-123)", - "[ABC-123]", - "ABC-123:", - } - falseTestCases := []string{ - "RC-08", - "PR-0", - "ABCDEFGHIJK-1", - "PR_1", - "", - "#ABC", - "", - "ABC", - "GG-", - "rm-1", - "/home/gitea/ABC-1234", - "MY-STRING-ABC-123", - } - - for _, testCase := range trueTestCases { - assert.True(t, issueAlphanumericPattern.MatchString(testCase)) - } - for _, testCase := range falseTestCases { - assert.False(t, issueAlphanumericPattern.MatchString(testCase)) - } -} - func TestRegExp_shortLinkPattern(t *testing.T) { trueTestCases := []string{ "[[stuff]]", diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 66e56f71a7..91ef320b40 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -89,6 +89,11 @@ func TestRender_links(t *testing.T) { } // Text that should be turned into URL + defaultCustom := setting.Markdown.CustomURLSchemes + setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"} + ReplaceSanitizer() + CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) + test( "https://www.example.com", `

https://www.example.com

`) @@ -131,6 +136,12 @@ func TestRender_links(t *testing.T) { test( "https://username:password@gitea.com", `

https://username:password@gitea.com

`) + test( + "ftp://gitea.com/file.txt", + `

ftp://gitea.com/file.txt

`) + test( + "magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download", + `

magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcadabcde&dn=download

`) // Test that should *not* be turned into URL test( @@ -154,6 +165,14 @@ func TestRender_links(t *testing.T) { test( "www", `

www

`) + test( + "ftps://gitea.com", + `

ftps://gitea.com

`) + + // Restore previous settings + setting.Markdown.CustomURLSchemes = defaultCustom + ReplaceSanitizer() + CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) } func TestRender_email(t *testing.T) { diff --git a/modules/markup/markup.go b/modules/markup/markup.go index dc43b533c0..008b21ab97 100644 --- a/modules/markup/markup.go +++ b/modules/markup/markup.go @@ -9,12 +9,16 @@ import ( "strings" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" ) // Init initialize regexps for markdown parsing func Init() { getIssueFullPattern() NewSanitizer() + if len(setting.Markdown.CustomURLSchemes) > 0 { + CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) + } // since setting maybe changed extensions, this will reload all parser extensions mapping extParsers = make(map[string]Parser) diff --git a/modules/markup/mdstripper/mdstripper.go b/modules/markup/mdstripper/mdstripper.go new file mode 100644 index 0000000000..7a901b17a9 --- /dev/null +++ b/modules/markup/mdstripper/mdstripper.go @@ -0,0 +1,260 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package mdstripper + +import ( + "bytes" + + "github.com/russross/blackfriday" +) + +// MarkdownStripper extends blackfriday.Renderer +type MarkdownStripper struct { + blackfriday.Renderer + links []string + coallesce bool +} + +const ( + blackfridayExtensions = 0 | + blackfriday.EXTENSION_NO_INTRA_EMPHASIS | + blackfriday.EXTENSION_TABLES | + blackfriday.EXTENSION_FENCED_CODE | + blackfriday.EXTENSION_STRIKETHROUGH | + blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK | + blackfriday.EXTENSION_DEFINITION_LISTS | + blackfriday.EXTENSION_FOOTNOTES | + blackfriday.EXTENSION_HEADER_IDS | + blackfriday.EXTENSION_AUTO_HEADER_IDS | + // Not included in modules/markup/markdown/markdown.go; + // required here to process inline links + blackfriday.EXTENSION_AUTOLINK +) + +//revive:disable:var-naming Implementing the Rendering interface requires breaking some linting rules + +// StripMarkdown parses markdown content by removing all markup and code blocks +// in order to extract links and other references +func StripMarkdown(rawBytes []byte) (string, []string) { + stripper := &MarkdownStripper{ + links: make([]string, 0, 10), + } + body := blackfriday.Markdown(rawBytes, stripper, blackfridayExtensions) + return string(body), stripper.GetLinks() +} + +// StripMarkdownBytes parses markdown content by removing all markup and code blocks +// in order to extract links and other references +func StripMarkdownBytes(rawBytes []byte) ([]byte, []string) { + stripper := &MarkdownStripper{ + links: make([]string, 0, 10), + } + body := blackfriday.Markdown(rawBytes, stripper, blackfridayExtensions) + return body, stripper.GetLinks() +} + +// block-level callbacks + +// BlockCode dummy function to proceed with rendering +func (r *MarkdownStripper) BlockCode(out *bytes.Buffer, text []byte, infoString string) { + // Not rendered + r.coallesce = false +} + +// BlockQuote dummy function to proceed with rendering +func (r *MarkdownStripper) BlockQuote(out *bytes.Buffer, text []byte) { + // FIXME: perhaps it's better to leave out block quote for this? + r.processString(out, text, false) +} + +// BlockHtml dummy function to proceed with rendering +func (r *MarkdownStripper) BlockHtml(out *bytes.Buffer, text []byte) { //nolint + // Not rendered + r.coallesce = false +} + +// Header dummy function to proceed with rendering +func (r *MarkdownStripper) Header(out *bytes.Buffer, text func() bool, level int, id string) { + text() + r.coallesce = false +} + +// HRule dummy function to proceed with rendering +func (r *MarkdownStripper) HRule(out *bytes.Buffer) { + // Not rendered + r.coallesce = false +} + +// List dummy function to proceed with rendering +func (r *MarkdownStripper) List(out *bytes.Buffer, text func() bool, flags int) { + text() + r.coallesce = false +} + +// ListItem dummy function to proceed with rendering +func (r *MarkdownStripper) ListItem(out *bytes.Buffer, text []byte, flags int) { + r.processString(out, text, false) +} + +// Paragraph dummy function to proceed with rendering +func (r *MarkdownStripper) Paragraph(out *bytes.Buffer, text func() bool) { + text() + r.coallesce = false +} + +// Table dummy function to proceed with rendering +func (r *MarkdownStripper) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) { + r.processString(out, header, false) + r.processString(out, body, false) +} + +// TableRow dummy function to proceed with rendering +func (r *MarkdownStripper) TableRow(out *bytes.Buffer, text []byte) { + r.processString(out, text, false) +} + +// TableHeaderCell dummy function to proceed with rendering +func (r *MarkdownStripper) TableHeaderCell(out *bytes.Buffer, text []byte, flags int) { + r.processString(out, text, false) +} + +// TableCell dummy function to proceed with rendering +func (r *MarkdownStripper) TableCell(out *bytes.Buffer, text []byte, flags int) { + r.processString(out, text, false) +} + +// Footnotes dummy function to proceed with rendering +func (r *MarkdownStripper) Footnotes(out *bytes.Buffer, text func() bool) { + text() +} + +// FootnoteItem dummy function to proceed with rendering +func (r *MarkdownStripper) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) { + r.processString(out, text, false) +} + +// TitleBlock dummy function to proceed with rendering +func (r *MarkdownStripper) TitleBlock(out *bytes.Buffer, text []byte) { + r.processString(out, text, false) +} + +// Span-level callbacks + +// AutoLink dummy function to proceed with rendering +func (r *MarkdownStripper) AutoLink(out *bytes.Buffer, link []byte, kind int) { + r.processLink(out, link, []byte{}) +} + +// CodeSpan dummy function to proceed with rendering +func (r *MarkdownStripper) CodeSpan(out *bytes.Buffer, text []byte) { + // Not rendered + r.coallesce = false +} + +// DoubleEmphasis dummy function to proceed with rendering +func (r *MarkdownStripper) DoubleEmphasis(out *bytes.Buffer, text []byte) { + r.processString(out, text, false) +} + +// Emphasis dummy function to proceed with rendering +func (r *MarkdownStripper) Emphasis(out *bytes.Buffer, text []byte) { + r.processString(out, text, false) +} + +// Image dummy function to proceed with rendering +func (r *MarkdownStripper) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { + // Not rendered + r.coallesce = false +} + +// LineBreak dummy function to proceed with rendering +func (r *MarkdownStripper) LineBreak(out *bytes.Buffer) { + // Not rendered + r.coallesce = false +} + +// Link dummy function to proceed with rendering +func (r *MarkdownStripper) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) { + r.processLink(out, link, content) +} + +// RawHtmlTag dummy function to proceed with rendering +func (r *MarkdownStripper) RawHtmlTag(out *bytes.Buffer, tag []byte) { //nolint + // Not rendered + r.coallesce = false +} + +// TripleEmphasis dummy function to proceed with rendering +func (r *MarkdownStripper) TripleEmphasis(out *bytes.Buffer, text []byte) { + r.processString(out, text, false) +} + +// StrikeThrough dummy function to proceed with rendering +func (r *MarkdownStripper) StrikeThrough(out *bytes.Buffer, text []byte) { + r.processString(out, text, false) +} + +// FootnoteRef dummy function to proceed with rendering +func (r *MarkdownStripper) FootnoteRef(out *bytes.Buffer, ref []byte, id int) { + // Not rendered + r.coallesce = false +} + +// Low-level callbacks + +// Entity dummy function to proceed with rendering +func (r *MarkdownStripper) Entity(out *bytes.Buffer, entity []byte) { + // FIXME: literal entities are not parsed; perhaps they should + r.coallesce = false +} + +// NormalText dummy function to proceed with rendering +func (r *MarkdownStripper) NormalText(out *bytes.Buffer, text []byte) { + r.processString(out, text, true) +} + +// Header and footer + +// DocumentHeader dummy function to proceed with rendering +func (r *MarkdownStripper) DocumentHeader(out *bytes.Buffer) { + r.coallesce = false +} + +// DocumentFooter dummy function to proceed with rendering +func (r *MarkdownStripper) DocumentFooter(out *bytes.Buffer) { + r.coallesce = false +} + +// GetFlags returns rendering flags +func (r *MarkdownStripper) GetFlags() int { + return 0 +} + +//revive:enable:var-naming + +func doubleSpace(out *bytes.Buffer) { + if out.Len() > 0 { + out.WriteByte('\n') + } +} + +func (r *MarkdownStripper) processString(out *bytes.Buffer, text []byte, coallesce bool) { + // Always break-up words + if !coallesce || !r.coallesce { + doubleSpace(out) + } + out.Write(text) + r.coallesce = coallesce +} +func (r *MarkdownStripper) processLink(out *bytes.Buffer, link []byte, content []byte) { + // Links are processed out of band + r.links = append(r.links, string(link)) + r.coallesce = false +} + +// GetLinks returns the list of link data collected while parsing +func (r *MarkdownStripper) GetLinks() []string { + return r.links +} diff --git a/modules/markup/mdstripper/mdstripper_test.go b/modules/markup/mdstripper/mdstripper_test.go new file mode 100644 index 0000000000..157fe1975b --- /dev/null +++ b/modules/markup/mdstripper/mdstripper_test.go @@ -0,0 +1,71 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package mdstripper + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMarkdownStripper(t *testing.T) { + type testItem struct { + markdown string + expectedText []string + expectedLinks []string + } + + list := []testItem{ + { + ` +## This is a title + +This is [one](link) to paradise. +This **is emphasized**. +This: should coallesce. + +` + "```" + ` +This is a code block. +This should not appear in the output at all. +` + "```" + ` + +* Bullet 1 +* Bullet 2 + +A HIDDEN ` + "`" + `GHOST` + "`" + ` IN THIS LINE. + `, + []string{ + "This is a title", + "This is", + "to paradise.", + "This", + "is emphasized", + ".", + "This: should coallesce.", + "Bullet 1", + "Bullet 2", + "A HIDDEN", + "IN THIS LINE.", + }, + []string{ + "link", + }}, + } + + for _, test := range list { + text, links := StripMarkdown([]byte(test.markdown)) + rawlines := strings.Split(text, "\n") + lines := make([]string, 0, len(rawlines)) + for _, line := range rawlines { + line := strings.TrimSpace(line) + if line != "" { + lines = append(lines, line) + } + } + assert.EqualValues(t, test.expectedText, lines) + assert.EqualValues(t, test.expectedLinks, links) + } +} diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 2ec43cf4fd..f873e8105e 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -28,19 +28,28 @@ var sanitizer = &Sanitizer{} // entire application lifecycle. func NewSanitizer() { sanitizer.init.Do(func() { - sanitizer.policy = bluemonday.UGCPolicy() - // We only want to allow HighlightJS specific classes for code blocks - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^language-\w+$`)).OnElements("code") - - // Checkboxes - sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") - sanitizer.policy.AllowAttrs("checked", "disabled").OnElements("input") - - // Custom URL-Schemes - sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) + ReplaceSanitizer() }) } +// ReplaceSanitizer replaces the current sanitizer to account for changes in settings +func ReplaceSanitizer() { + sanitizer = &Sanitizer{} + sanitizer.policy = bluemonday.UGCPolicy() + // We only want to allow HighlightJS specific classes for code blocks + sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^language-\w+$`)).OnElements("code") + + // Checkboxes + sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") + sanitizer.policy.AllowAttrs("checked", "disabled").OnElements("input") + + // Custom URL-Schemes + sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) + + // Allow keyword markup + sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^` + keywordClass + `$`)).OnElements("span") +} + // Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist. func Sanitize(s string) string { NewSanitizer() diff --git a/modules/migrations/base/downloader.go b/modules/migrations/base/downloader.go index ab5ca6dec8..69c2adb9e9 100644 --- a/modules/migrations/base/downloader.go +++ b/modules/migrations/base/downloader.go @@ -5,6 +5,8 @@ package base +import "code.gitea.io/gitea/modules/structs" + // Downloader downloads the site repo informations type Downloader interface { GetRepoInfo() (*Repository, error) @@ -21,4 +23,5 @@ type Downloader interface { type DownloaderFactory interface { Match(opts MigrateOptions) (bool, error) New(opts MigrateOptions) (Downloader, error) + GitServiceType() structs.GitServiceType } diff --git a/modules/migrations/base/options.go b/modules/migrations/base/options.go index ba7fdc6815..2d180b61d9 100644 --- a/modules/migrations/base/options.go +++ b/modules/migrations/base/options.go @@ -5,22 +5,7 @@ package base -// MigrateOptions defines the way a repository gets migrated -type MigrateOptions struct { - RemoteURL string - AuthUsername string - AuthPassword string - Name string - Description string - OriginalURL string +import "code.gitea.io/gitea/modules/structs" - Wiki bool - Issues bool - Milestones bool - Labels bool - Releases bool - Comments bool - PullRequests bool - Private bool - Mirror bool -} +// MigrateOptions defines the way a repository gets migrated +type MigrateOptions = structs.MigrateRepoOption diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index 1edac47a6e..a095751c6b 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" gouuid "github.com/satori/go.uuid" @@ -33,15 +34,17 @@ var ( // GiteaLocalUploader implements an Uploader to gitea sites type GiteaLocalUploader struct { - doer *models.User - repoOwner string - repoName string - repo *models.Repository - labels sync.Map - milestones sync.Map - issues sync.Map - gitRepo *git.Repository - prHeadCache map[string]struct{} + doer *models.User + repoOwner string + repoName string + repo *models.Repository + labels sync.Map + milestones sync.Map + issues sync.Map + gitRepo *git.Repository + prHeadCache map[string]struct{} + userMap map[int64]int64 // external user id mapping to user id + gitServiceType structs.GitServiceType } // NewGiteaLocalUploader creates an gitea Uploader via gitea API v1 @@ -51,6 +54,7 @@ func NewGiteaLocalUploader(doer *models.User, repoOwner, repoName string) *Gitea repoOwner: repoOwner, repoName: repoName, prHeadCache: make(map[string]struct{}), + userMap: make(map[int64]int64), } } @@ -90,16 +94,35 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate remoteAddr = u.String() } - r, err := models.MigrateRepository(g.doer, owner, models.MigrateRepoOptions{ - Name: g.repoName, - Description: repo.Description, - OriginalURL: repo.OriginalURL, - IsMirror: repo.IsMirror, - RemoteAddr: remoteAddr, - IsPrivate: repo.IsPrivate, - Wiki: opts.Wiki, - SyncReleasesWithTags: !opts.Releases, // if didn't get releases, then sync them from tags + var r *models.Repository + if opts.MigrateToRepoID <= 0 { + r, err = models.CreateRepository(g.doer, owner, models.CreateRepoOptions{ + Name: g.repoName, + Description: repo.Description, + OriginalURL: repo.OriginalURL, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: models.RepositoryBeingMigrated, + }) + } else { + r, err = models.GetRepositoryByID(opts.MigrateToRepoID) + } + if err != nil { + return err + } + + r, err = models.MigrateRepositoryGitData(g.doer, owner, r, structs.MigrateRepoOption{ + RepoName: g.repoName, + Description: repo.Description, + OriginalURL: repo.OriginalURL, + GitServiceType: opts.GitServiceType, + Mirror: repo.IsMirror, + CloneAddr: remoteAddr, + Private: repo.IsPrivate, + Wiki: opts.Wiki, + Releases: opts.Releases, // if didn't get releases, then sync them from tags }) + g.repo = r if err != nil { return err @@ -175,20 +198,38 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { var rels = make([]*models.Release, 0, len(releases)) for _, release := range releases { var rel = models.Release{ - RepoID: g.repo.ID, - PublisherID: g.doer.ID, - TagName: release.TagName, - LowerTagName: strings.ToLower(release.TagName), - Target: release.TargetCommitish, - Title: release.Name, - Sha1: release.TargetCommitish, - Note: release.Body, - IsDraft: release.Draft, - IsPrerelease: release.Prerelease, - IsTag: false, - CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), - OriginalAuthor: release.PublisherName, - OriginalAuthorID: release.PublisherID, + RepoID: g.repo.ID, + TagName: release.TagName, + LowerTagName: strings.ToLower(release.TagName), + Target: release.TargetCommitish, + Title: release.Name, + Sha1: release.TargetCommitish, + Note: release.Body, + IsDraft: release.Draft, + IsPrerelease: release.Prerelease, + IsTag: false, + CreatedUnix: timeutil.TimeStamp(release.Created.Unix()), + } + + userid, ok := g.userMap[release.PublisherID] + tp := g.gitServiceType.Name() + if !ok && tp != "" { + var err error + userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", release.PublisherID)) + if err != nil { + log.Error("GetUserIDByExternalUserID: %v", err) + } + if userid > 0 { + g.userMap[release.PublisherID] = userid + } + } + + if userid > 0 { + rel.PublisherID = userid + } else { + rel.PublisherID = g.doer.ID + rel.OriginalAuthor = release.PublisherName + rel.OriginalAuthorID = release.PublisherID } // calc NumCommits @@ -266,20 +307,39 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { } var is = models.Issue{ - RepoID: g.repo.ID, - Repo: g.repo, - Index: issue.Number, - PosterID: g.doer.ID, - OriginalAuthor: issue.PosterName, - OriginalAuthorID: issue.PosterID, - Title: issue.Title, - Content: issue.Content, - IsClosed: issue.State == "closed", - IsLocked: issue.IsLocked, - MilestoneID: milestoneID, - Labels: labels, - CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()), + RepoID: g.repo.ID, + Repo: g.repo, + Index: issue.Number, + Title: issue.Title, + Content: issue.Content, + IsClosed: issue.State == "closed", + IsLocked: issue.IsLocked, + MilestoneID: milestoneID, + Labels: labels, + CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()), } + + userid, ok := g.userMap[issue.PosterID] + tp := g.gitServiceType.Name() + if !ok && tp != "" { + var err error + userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", issue.PosterID)) + if err != nil { + log.Error("GetUserIDByExternalUserID: %v", err) + } + if userid > 0 { + g.userMap[issue.PosterID] = userid + } + } + + if userid > 0 { + is.PosterID = userid + } else { + is.PosterID = g.doer.ID + is.OriginalAuthor = issue.PosterName + is.OriginalAuthorID = issue.PosterID + } + if issue.Closed != nil { is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix()) } @@ -313,15 +373,35 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { issueID = issueIDStr.(int64) } - cms = append(cms, &models.Comment{ - IssueID: issueID, - Type: models.CommentTypeComment, - PosterID: g.doer.ID, - OriginalAuthor: comment.PosterName, - OriginalAuthorID: comment.PosterID, - Content: comment.Content, - CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()), - }) + userid, ok := g.userMap[comment.PosterID] + tp := g.gitServiceType.Name() + if !ok && tp != "" { + var err error + userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", comment.PosterID)) + if err != nil { + log.Error("GetUserIDByExternalUserID: %v", err) + } + if userid > 0 { + g.userMap[comment.PosterID] = userid + } + } + + cm := models.Comment{ + IssueID: issueID, + Type: models.CommentTypeComment, + Content: comment.Content, + CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()), + } + + if userid > 0 { + cm.PosterID = userid + } else { + cm.PosterID = g.doer.ID + cm.OriginalAuthor = comment.PosterName + cm.OriginalAuthorID = comment.PosterID + } + + cms = append(cms, &cm) // TODO: Reactions } @@ -337,6 +417,28 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error if err != nil { return err } + + userid, ok := g.userMap[pr.PosterID] + tp := g.gitServiceType.Name() + if !ok && tp != "" { + var err error + userid, err = models.GetUserIDByExternalUserID(tp, fmt.Sprintf("%v", pr.PosterID)) + if err != nil { + log.Error("GetUserIDByExternalUserID: %v", err) + } + if userid > 0 { + g.userMap[pr.PosterID] = userid + } + } + + if userid > 0 { + gpr.Issue.PosterID = userid + } else { + gpr.Issue.PosterID = g.doer.ID + gpr.Issue.OriginalAuthor = pr.PosterName + gpr.Issue.OriginalAuthorID = pr.PosterID + } + gprs = append(gprs, gpr) } if err := models.InsertPullRequests(gprs...); err != nil { @@ -442,32 +544,50 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR head = pr.Head.Ref } - var pullRequest = models.PullRequest{ - HeadRepoID: g.repo.ID, - HeadBranch: head, - HeadUserName: g.repoOwner, - BaseRepoID: g.repo.ID, - BaseBranch: pr.Base.Ref, - MergeBase: pr.Base.SHA, - Index: pr.Number, - HasMerged: pr.Merged, + var issue = models.Issue{ + RepoID: g.repo.ID, + Repo: g.repo, + Title: pr.Title, + Index: pr.Number, + Content: pr.Content, + MilestoneID: milestoneID, + IsPull: true, + IsClosed: pr.State == "closed", + IsLocked: pr.IsLocked, + Labels: labels, + CreatedUnix: timeutil.TimeStamp(pr.Created.Unix()), + } - Issue: &models.Issue{ - RepoID: g.repo.ID, - Repo: g.repo, - Title: pr.Title, - Index: pr.Number, - PosterID: g.doer.ID, - OriginalAuthor: pr.PosterName, - OriginalAuthorID: pr.PosterID, - Content: pr.Content, - MilestoneID: milestoneID, - IsPull: true, - IsClosed: pr.State == "closed", - IsLocked: pr.IsLocked, - Labels: labels, - CreatedUnix: timeutil.TimeStamp(pr.Created.Unix()), - }, + userid, ok := g.userMap[pr.PosterID] + if !ok { + var err error + userid, err = models.GetUserIDByExternalUserID("github", fmt.Sprintf("%v", pr.PosterID)) + if err != nil { + log.Error("GetUserIDByExternalUserID: %v", err) + } + if userid > 0 { + g.userMap[pr.PosterID] = userid + } + } + + if userid > 0 { + issue.PosterID = userid + } else { + issue.PosterID = g.doer.ID + issue.OriginalAuthor = pr.PosterName + issue.OriginalAuthorID = pr.PosterID + } + + var pullRequest = models.PullRequest{ + HeadRepoID: g.repo.ID, + HeadBranch: head, + BaseRepoID: g.repo.ID, + BaseBranch: pr.Base.Ref, + MergeBase: pr.Base.SHA, + Index: pr.Number, + HasMerged: pr.Merged, + + Issue: &issue, } if pullRequest.Issue.IsClosed && pr.Closed != nil { diff --git a/modules/migrations/gitea_test.go b/modules/migrations/gitea_test.go index 88a3a6d218..73c119a15d 100644 --- a/modules/migrations/gitea_test.go +++ b/modules/migrations/gitea_test.go @@ -10,6 +10,7 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -29,9 +30,9 @@ func TestGiteaUploadRepo(t *testing.T) { uploader = NewGiteaLocalUploader(user, user.Name, repoName) ) - err := migrateRepository(downloader, uploader, MigrateOptions{ - RemoteURL: "https://github.com/go-xorm/builder", - Name: repoName, + err := migrateRepository(downloader, uploader, structs.MigrateRepoOption{ + CloneAddr: "https://github.com/go-xorm/builder", + RepoName: repoName, AuthUsername: "", Wiki: true, diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 754f98941c..00d137a3de 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/structs" "github.com/google/go-github/v24/github" "golang.org/x/oauth2" @@ -34,17 +35,17 @@ type GithubDownloaderV3Factory struct { // Match returns ture if the migration remote URL matched this downloader factory func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error) { - u, err := url.Parse(opts.RemoteURL) + u, err := url.Parse(opts.CloneAddr) if err != nil { return false, err } - return u.Host == "github.com" && opts.AuthUsername != "", nil + return strings.EqualFold(u.Host, "github.com") && opts.AuthUsername != "", nil } // New returns a Downloader related to this factory according MigrateOptions func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Downloader, error) { - u, err := url.Parse(opts.RemoteURL) + u, err := url.Parse(opts.CloneAddr) if err != nil { return nil, err } @@ -58,6 +59,11 @@ func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Download return NewGithubDownloaderV3(opts.AuthUsername, opts.AuthPassword, oldOwner, oldName), nil } +// GitServiceType returns the type of git service +func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType { + return structs.GithubService +} + // GithubDownloaderV3 implements a Downloader interface to get repository informations // from github via APIv3 type GithubDownloaderV3 struct { diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 27782cb940..bbc1dc2d56 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -6,9 +6,12 @@ package migrations import ( + "fmt" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/structs" ) // MigrateOptions is equal to base.MigrateOptions @@ -27,7 +30,8 @@ func RegisterDownloaderFactory(factory base.DownloaderFactory) { func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOptions) (*models.Repository, error) { var ( downloader base.Downloader - uploader = NewGiteaLocalUploader(doer, ownerName, opts.Name) + uploader = NewGiteaLocalUploader(doer, ownerName, opts.RepoName) + theFactory base.DownloaderFactory ) for _, factory := range factories { @@ -38,6 +42,7 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt if err != nil { return nil, err } + theFactory = factory break } } @@ -50,14 +55,22 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt opts.Comments = false opts.Issues = false opts.PullRequests = false - downloader = NewPlainGitDownloader(ownerName, opts.Name, opts.RemoteURL) - log.Trace("Will migrate from git: %s", opts.RemoteURL) + opts.GitServiceType = structs.PlainGitService + downloader = NewPlainGitDownloader(ownerName, opts.RepoName, opts.CloneAddr) + log.Trace("Will migrate from git: %s", opts.CloneAddr) + } else if opts.GitServiceType == structs.NotMigrated { + opts.GitServiceType = theFactory.GitServiceType() } + uploader.gitServiceType = opts.GitServiceType if err := migrateRepository(downloader, uploader, opts); err != nil { if err1 := uploader.Rollback(); err1 != nil { log.Error("rollback failed: %v", err1) } + + if err2 := models.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.CloneAddr, err)); err2 != nil { + log.Error("create respotiry notice failed: ", err2) + } return nil, err } diff --git a/modules/migrations/update.go b/modules/migrations/update.go new file mode 100644 index 0000000000..d1465b2baf --- /dev/null +++ b/modules/migrations/update.go @@ -0,0 +1,53 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/structs" +) + +// UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID +func UpdateMigrationPosterID() { + for _, gitService := range structs.SupportedFullGitService { + if err := updateMigrationPosterIDByGitService(gitService); err != nil { + log.Error("updateMigrationPosterIDByGitService failed: %v", err) + } + } +} + +func updateMigrationPosterIDByGitService(tp structs.GitServiceType) error { + provider := tp.Name() + if len(provider) == 0 { + return nil + } + + const batchSize = 100 + var start int + for { + users, err := models.FindExternalUsersByProvider(models.FindExternalUserOptions{ + Provider: provider, + Start: start, + Limit: batchSize, + }) + if err != nil { + return err + } + + for _, user := range users { + externalUserID := user.ExternalID + if err := models.UpdateMigrationsByType(tp, externalUserID, user.UserID); err != nil { + log.Error("UpdateMigrationsByType type %s external user id %v to local user id %v failed: %v", tp.Name(), user.ExternalID, user.UserID, err) + } + } + + if len(users) < batchSize { + break + } + start += len(users) + } + return nil +} diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index e44f3cc632..c74bb52014 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -21,7 +21,7 @@ type Notifier interface { NotifyNewIssue(*models.Issue) NotifyIssueChangeStatus(*models.User, *models.Issue, bool) NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue) - NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, removed bool) + NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) NotifyIssueClearLabels(doer *models.User, issue *models.Issue) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 12be1999f9..9fb08884a1 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -83,7 +83,7 @@ func (*NullNotifier) NotifyIssueChangeContent(doer *models.User, issue *models.I } // NotifyIssueChangeAssignee places a place holder function -func (*NullNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, removed bool) { +func (*NullNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) { } // NotifyIssueClearLabels places a place holder function diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go index e1ae391f78..0900c6dcdf 100644 --- a/modules/notification/mail/mail.go +++ b/modules/notification/mail/mail.go @@ -5,6 +5,8 @@ package mail import ( + "fmt" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification/base" @@ -88,3 +90,11 @@ func (m *mailNotifier) NotifyPullRequestReview(pr *models.PullRequest, r *models log.Error("MailParticipants: %v", err) } } + +func (m *mailNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) { + // mail only sent to added assignees and not self-assignee + if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() == models.EmailNotificationsEnabled { + ct := fmt.Sprintf("Assigned #%d.", issue.Index) + mailer.SendIssueAssignedMail(issue, doer, ct, comment, []string{assignee.Email}) + } +} diff --git a/modules/notification/notification.go b/modules/notification/notification.go index e0de88346d..0f1b63cf67 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -11,6 +11,8 @@ import ( "code.gitea.io/gitea/modules/notification/indexer" "code.gitea.io/gitea/modules/notification/mail" "code.gitea.io/gitea/modules/notification/ui" + "code.gitea.io/gitea/modules/notification/webhook" + "code.gitea.io/gitea/modules/setting" ) var ( @@ -23,10 +25,14 @@ func RegisterNotifier(notifier base.Notifier) { notifiers = append(notifiers, notifier) } -func init() { +// NewContext registers notification handlers +func NewContext() { RegisterNotifier(ui.NewNotifier()) - RegisterNotifier(mail.NewNotifier()) + if setting.Service.EnableNotifyMail { + RegisterNotifier(mail.NewNotifier()) + } RegisterNotifier(indexer.NewNotifier()) + RegisterNotifier(webhook.NewNotifier()) } // NotifyCreateIssueComment notifies issue comment related message to notifiers @@ -136,9 +142,9 @@ func NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent } // NotifyIssueChangeAssignee notifies change content to notifiers -func NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, removed bool) { +func NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) { for _, notifier := range notifiers { - notifier.NotifyIssueChangeAssignee(doer, issue, removed) + notifier.NotifyIssueChangeAssignee(doer, issue, assignee, removed, comment) } } diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go new file mode 100644 index 0000000000..33adfaa739 --- /dev/null +++ b/modules/notification/webhook/webhook.go @@ -0,0 +1,67 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package webhook + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/notification/base" + api "code.gitea.io/gitea/modules/structs" +) + +type webhookNotifier struct { + base.NullNotifier +} + +var ( + _ base.Notifier = &webhookNotifier{} +) + +// NewNotifier create a new webhookNotifier notifier +func NewNotifier() base.Notifier { + return &webhookNotifier{} +} + +func (m *webhookNotifier) NotifyIssueClearLabels(doer *models.User, issue *models.Issue) { + if err := issue.LoadPoster(); err != nil { + log.Error("loadPoster: %v", err) + return + } + + if err := issue.LoadRepo(); err != nil { + log.Error("LoadRepo: %v", err) + return + } + + mode, _ := models.AccessLevel(issue.Poster, issue.Repo) + var err error + if issue.IsPull { + if err = issue.LoadPullRequest(); err != nil { + log.Error("LoadPullRequest: %v", err) + return + } + + err = models.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + Action: api.HookIssueLabelCleared, + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }) + } else { + err = models.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + Action: api.HookIssueLabelCleared, + Index: issue.Index, + Issue: issue.APIFormat(), + Repository: issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }) + } + if err != nil { + log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) + } else { + go models.HookQueue.Add(issue.RepoID) + } +} diff --git a/modules/password/password.go b/modules/password/password.go new file mode 100644 index 0000000000..92986977ec --- /dev/null +++ b/modules/password/password.go @@ -0,0 +1,88 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package password + +import ( + "crypto/rand" + "math/big" + "strings" + "sync" + + "code.gitea.io/gitea/modules/setting" +) + +var ( + matchComplexityOnce sync.Once + validChars string + requiredChars []string + + charComplexities = map[string]string{ + "lower": `abcdefghijklmnopqrstuvwxyz`, + "upper": `ABCDEFGHIJKLMNOPQRSTUVWXYZ`, + "digit": `0123456789`, + "spec": ` !"#$%&'()*+,-./:;<=>?@[\]^_{|}~` + "`", + } +) + +// NewComplexity for preparation +func NewComplexity() { + matchComplexityOnce.Do(func() { + setupComplexity(setting.PasswordComplexity) + }) +} + +func setupComplexity(values []string) { + if len(values) != 1 || values[0] != "off" { + for _, val := range values { + if chars, ok := charComplexities[val]; ok { + validChars += chars + requiredChars = append(requiredChars, chars) + } + } + if len(requiredChars) == 0 { + // No valid character classes found; use all classes as default + for _, chars := range charComplexities { + validChars += chars + requiredChars = append(requiredChars, chars) + } + } + } + if validChars == "" { + // No complexities to check; provide a sensible default for password generation + validChars = charComplexities["lower"] + charComplexities["upper"] + charComplexities["digit"] + } +} + +// IsComplexEnough return True if password meets complexity settings +func IsComplexEnough(pwd string) bool { + NewComplexity() + if len(validChars) > 0 { + for _, req := range requiredChars { + if !strings.ContainsAny(req, pwd) { + return false + } + } + } + return true +} + +// Generate a random password +func Generate(n int) (string, error) { + NewComplexity() + buffer := make([]byte, n) + max := big.NewInt(int64(len(validChars))) + for { + for j := 0; j < n; j++ { + rnd, err := rand.Int(rand.Reader, max) + if err != nil { + return "", err + } + buffer[j] = validChars[rnd.Int64()] + } + if IsComplexEnough(string(buffer)) && string(buffer[0]) != " " && string(buffer[n-1]) != " " { + return string(buffer), nil + } + } +} diff --git a/modules/password/password_test.go b/modules/password/password_test.go new file mode 100644 index 0000000000..d46a6d1571 --- /dev/null +++ b/modules/password/password_test.go @@ -0,0 +1,75 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package password + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestComplexity_IsComplexEnough(t *testing.T) { + matchComplexityOnce.Do(func() {}) + + testlist := []struct { + complexity []string + truevalues []string + falsevalues []string + }{ + {[]string{"lower"}, []string{"abc", "abc!"}, []string{"ABC", "123", "=!$", ""}}, + {[]string{"upper"}, []string{"ABC"}, []string{"abc", "123", "=!$", "abc!", ""}}, + {[]string{"digit"}, []string{"123"}, []string{"abc", "ABC", "=!$", "abc!", ""}}, + {[]string{"spec"}, []string{"=!$", "abc!"}, []string{"abc", "ABC", "123", ""}}, + {[]string{"off"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}, nil}, + {[]string{"lower", "spec"}, []string{"abc!"}, []string{"abc", "ABC", "123", "=!$", "abcABC123", ""}}, + {[]string{"lower", "upper", "digit"}, []string{"abcABC123"}, []string{"abc", "ABC", "123", "=!$", "abc!", ""}}, + } + + for _, test := range testlist { + testComplextity(test.complexity) + for _, val := range test.truevalues { + assert.True(t, IsComplexEnough(val)) + } + for _, val := range test.falsevalues { + assert.False(t, IsComplexEnough(val)) + } + } + + // Remove settings for other tests + testComplextity([]string{"off"}) +} + +func TestComplexity_Generate(t *testing.T) { + matchComplexityOnce.Do(func() {}) + + const maxCount = 50 + const pwdLen = 50 + + test := func(t *testing.T, modes []string) { + testComplextity(modes) + for i := 0; i < maxCount; i++ { + pwd, err := Generate(pwdLen) + assert.NoError(t, err) + assert.Equal(t, pwdLen, len(pwd)) + assert.True(t, IsComplexEnough(pwd), "Failed complexities with modes %+v for generated: %s", modes, pwd) + } + } + + test(t, []string{"lower"}) + test(t, []string{"upper"}) + test(t, []string{"lower", "upper", "spec"}) + test(t, []string{"off"}) + test(t, []string{""}) + + // Remove settings for other tests + testComplextity([]string{"off"}) +} + +func testComplextity(values []string) { + // Cleanup previous values + validChars = "" + requiredChars = make([]string, 0, len(values)) + setupComplexity(values) +} diff --git a/modules/private/hook.go b/modules/private/hook.go index 67496b5132..cc9703cc77 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -31,11 +31,12 @@ type HookOptions struct { GitAlternativeObjectDirectories string GitQuarantinePath string ProtectedBranchID int64 + IsDeployKey bool } // HookPreReceive check whether the provided commits are allowed func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&gitObjectDirectory=%s&gitAlternativeObjectDirectories=%s&gitQuarantinePath=%s&prID=%d", + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&gitObjectDirectory=%s&gitAlternativeObjectDirectories=%s&gitQuarantinePath=%s&prID=%d&isDeployKey=%t", url.PathEscape(ownerName), url.PathEscape(repoName), url.QueryEscape(opts.OldCommitID), @@ -46,6 +47,7 @@ func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string) url.QueryEscape(opts.GitAlternativeObjectDirectories), url.QueryEscape(opts.GitQuarantinePath), opts.ProtectedBranchID, + opts.IsDeployKey, ) resp, err := newInternalRequest(reqURL, "GET").Response() diff --git a/modules/process/manager.go b/modules/process/manager.go index 9ac3af86f1..3e77c0a6a9 100644 --- a/modules/process/manager.go +++ b/modules/process/manager.go @@ -1,4 +1,5 @@ // Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -9,6 +10,7 @@ import ( "context" "errors" "fmt" + "io" "os/exec" "sync" "time" @@ -93,6 +95,14 @@ func (pm *Manager) ExecDir(timeout time.Duration, dir, desc, cmdName string, arg // Returns its complete stdout and stderr // outputs and an error, if any (including timeout) func (pm *Manager) ExecDirEnv(timeout time.Duration, dir, desc string, env []string, cmdName string, args ...string) (string, string, error) { + return pm.ExecDirEnvStdIn(timeout, dir, desc, env, nil, cmdName, args...) +} + +// ExecDirEnvStdIn runs a command in given path and environment variables with provided stdIN, and waits for its completion +// up to the given timeout (or DefaultTimeout if -1 is given). +// Returns its complete stdout and stderr +// outputs and an error, if any (including timeout) +func (pm *Manager) ExecDirEnvStdIn(timeout time.Duration, dir, desc string, env []string, stdIn io.Reader, cmdName string, args ...string) (string, string, error) { if timeout == -1 { timeout = 60 * time.Second } @@ -108,6 +118,10 @@ func (pm *Manager) ExecDirEnv(timeout time.Duration, dir, desc string, env []str cmd.Env = env cmd.Stdout = stdOut cmd.Stderr = stdErr + if stdIn != nil { + cmd.Stdin = stdIn + } + if err := cmd.Start(); err != nil { return "", "", err } diff --git a/modules/references/references.go b/modules/references/references.go new file mode 100644 index 0000000000..9c74d0d081 --- /dev/null +++ b/modules/references/references.go @@ -0,0 +1,322 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package references + +import ( + "net/url" + "regexp" + "strconv" + "strings" + "sync" + + "code.gitea.io/gitea/modules/markup/mdstripper" + "code.gitea.io/gitea/modules/setting" +) + +var ( + // validNamePattern performs only the most basic validation for user or repository names + // Repository name should contain only alphanumeric, dash ('-'), underscore ('_') and dot ('.') characters. + validNamePattern = regexp.MustCompile(`^[a-z0-9_.-]+$`) + + // NOTE: All below regex matching do not perform any extra validation. + // Thus a link is produced even if the linked entity does not exist. + // While fast, this is also incorrect and lead to false positives. + // TODO: fix invalid linking issue + + // mentionPattern matches all mentions in the form of "@user" + mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_\.]+)(?:\s|$|\)|\])`) + // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 + issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`) + // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 + issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`) + // crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository + // e.g. gogits/gogs#12345 + crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+#[0-9]+)(?:\s|$|\)|\]|\.(\s|$))`) + + // Same as GitHub. See + // https://help.github.com/articles/closing-issues-via-commit-messages + issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"} + issueReopenKeywords = []string{"reopen", "reopens", "reopened"} + + issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp + + giteaHostInit sync.Once + giteaHost string +) + +// XRefAction represents the kind of effect a cross reference has once is resolved +type XRefAction int64 + +const ( + // XRefActionNone means the cross-reference is simply a comment + XRefActionNone XRefAction = iota // 0 + // XRefActionCloses means the cross-reference should close an issue if it is resolved + XRefActionCloses // 1 + // XRefActionReopens means the cross-reference should reopen an issue if it is resolved + XRefActionReopens // 2 + // XRefActionNeutered means the cross-reference will no longer affect the source + XRefActionNeutered // 3 +) + +// IssueReference contains an unverified cross-reference to a local issue or pull request +type IssueReference struct { + Index int64 + Owner string + Name string + Action XRefAction +} + +// RenderizableReference contains an unverified cross-reference to with rendering information +type RenderizableReference struct { + Issue string + Owner string + Name string + RefLocation *RefSpan + Action XRefAction + ActionLocation *RefSpan +} + +type rawReference struct { + index int64 + owner string + name string + action XRefAction + issue string + refLocation *RefSpan + actionLocation *RefSpan +} + +func rawToIssueReferenceList(reflist []*rawReference) []IssueReference { + refarr := make([]IssueReference, len(reflist)) + for i, r := range reflist { + refarr[i] = IssueReference{ + Index: r.index, + Owner: r.owner, + Name: r.name, + Action: r.action, + } + } + return refarr +} + +// RefSpan is the position where the reference was found within the parsed text +type RefSpan struct { + Start int + End int +} + +func makeKeywordsPat(keywords []string) *regexp.Regexp { + return regexp.MustCompile(`(?i)(?:\s|^|\(|\[)(` + strings.Join(keywords, `|`) + `):? $`) +} + +func init() { + issueCloseKeywordsPat = makeKeywordsPat(issueCloseKeywords) + issueReopenKeywordsPat = makeKeywordsPat(issueReopenKeywords) +} + +// getGiteaHostName returns a normalized string with the local host name, with no scheme or port information +func getGiteaHostName() string { + giteaHostInit.Do(func() { + if uapp, err := url.Parse(setting.AppURL); err == nil { + giteaHost = strings.ToLower(uapp.Host) + } else { + giteaHost = "" + } + }) + return giteaHost +} + +// FindAllMentionsMarkdown matches mention patterns in given content and +// returns a list of found unvalidated user names **not including** the @ prefix. +func FindAllMentionsMarkdown(content string) []string { + bcontent, _ := mdstripper.StripMarkdownBytes([]byte(content)) + locations := FindAllMentionsBytes(bcontent) + mentions := make([]string, len(locations)) + for i, val := range locations { + mentions[i] = string(bcontent[val.Start+1 : val.End]) + } + return mentions +} + +// FindAllMentionsBytes matches mention patterns in given content +// and returns a list of locations for the unvalidated user names, including the @ prefix. +func FindAllMentionsBytes(content []byte) []RefSpan { + mentions := mentionPattern.FindAllSubmatchIndex(content, -1) + ret := make([]RefSpan, len(mentions)) + for i, val := range mentions { + ret[i] = RefSpan{Start: val[2], End: val[3]} + } + return ret +} + +// FindFirstMentionBytes matches the first mention in then given content +// and returns the location of the unvalidated user name, including the @ prefix. +func FindFirstMentionBytes(content []byte) (bool, RefSpan) { + mention := mentionPattern.FindSubmatchIndex(content) + if mention == nil { + return false, RefSpan{} + } + return true, RefSpan{Start: mention[2], End: mention[3]} +} + +// FindAllIssueReferencesMarkdown strips content from markdown markup +// and returns a list of unvalidated references found in it. +func FindAllIssueReferencesMarkdown(content string) []IssueReference { + return rawToIssueReferenceList(findAllIssueReferencesMarkdown(content)) +} + +func findAllIssueReferencesMarkdown(content string) []*rawReference { + bcontent, links := mdstripper.StripMarkdownBytes([]byte(content)) + return findAllIssueReferencesBytes(bcontent, links) +} + +// FindAllIssueReferences returns a list of unvalidated references found in a string. +func FindAllIssueReferences(content string) []IssueReference { + return rawToIssueReferenceList(findAllIssueReferencesBytes([]byte(content), []string{})) +} + +// FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string. +func FindRenderizableReferenceNumeric(content string) (bool, *RenderizableReference) { + match := issueNumericPattern.FindStringSubmatchIndex(content) + if match == nil { + if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil { + return false, nil + } + } + r := getCrossReference([]byte(content), match[2], match[3], false) + if r == nil { + return false, nil + } + + return true, &RenderizableReference{ + Issue: r.issue, + Owner: r.owner, + Name: r.name, + RefLocation: r.refLocation, + Action: r.action, + ActionLocation: r.actionLocation, + } +} + +// FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string. +func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) { + match := issueAlphanumericPattern.FindStringSubmatchIndex(content) + if match == nil { + return false, nil + } + + action, location := findActionKeywords([]byte(content), match[2]) + + return true, &RenderizableReference{ + Issue: string(content[match[2]:match[3]]), + RefLocation: &RefSpan{Start: match[2], End: match[3]}, + Action: action, + ActionLocation: location, + } +} + +// FindAllIssueReferencesBytes returns a list of unvalidated references found in a byte slice. +func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference { + + ret := make([]*rawReference, 0, 10) + + matches := issueNumericPattern.FindAllSubmatchIndex(content, -1) + for _, match := range matches { + if ref := getCrossReference(content, match[2], match[3], false); ref != nil { + ret = append(ret, ref) + } + } + + matches = crossReferenceIssueNumericPattern.FindAllSubmatchIndex(content, -1) + for _, match := range matches { + if ref := getCrossReference(content, match[2], match[3], false); ref != nil { + ret = append(ret, ref) + } + } + + localhost := getGiteaHostName() + for _, link := range links { + if u, err := url.Parse(link); err == nil { + // Note: we're not attempting to match the URL scheme (http/https) + host := strings.ToLower(u.Host) + if host != "" && host != localhost { + continue + } + parts := strings.Split(u.EscapedPath(), "/") + // /user/repo/issues/3 + if len(parts) != 5 || parts[0] != "" { + continue + } + if parts[3] != "issues" && parts[3] != "pulls" { + continue + } + // Note: closing/reopening keywords not supported with URLs + bytes := []byte(parts[1] + "/" + parts[2] + "#" + parts[4]) + if ref := getCrossReference(bytes, 0, len(bytes), true); ref != nil { + ref.refLocation = nil + ret = append(ret, ref) + } + } + } + + return ret +} + +func getCrossReference(content []byte, start, end int, fromLink bool) *rawReference { + refid := string(content[start:end]) + parts := strings.Split(refid, "#") + if len(parts) != 2 { + return nil + } + repo, issue := parts[0], parts[1] + index, err := strconv.ParseInt(issue, 10, 64) + if err != nil { + return nil + } + if repo == "" { + if fromLink { + // Markdown links must specify owner/repo + return nil + } + action, location := findActionKeywords(content, start) + return &rawReference{ + index: index, + action: action, + issue: issue, + refLocation: &RefSpan{Start: start, End: end}, + actionLocation: location, + } + } + parts = strings.Split(strings.ToLower(repo), "/") + if len(parts) != 2 { + return nil + } + owner, name := parts[0], parts[1] + if !validNamePattern.MatchString(owner) || !validNamePattern.MatchString(name) { + return nil + } + action, location := findActionKeywords(content, start) + return &rawReference{ + index: index, + owner: owner, + name: name, + action: action, + issue: issue, + refLocation: &RefSpan{Start: start, End: end}, + actionLocation: location, + } +} + +func findActionKeywords(content []byte, start int) (XRefAction, *RefSpan) { + m := issueCloseKeywordsPat.FindSubmatchIndex(content[:start]) + if m != nil { + return XRefActionCloses, &RefSpan{Start: m[2], End: m[3]} + } + m = issueReopenKeywordsPat.FindSubmatchIndex(content[:start]) + if m != nil { + return XRefActionReopens, &RefSpan{Start: m[2], End: m[3]} + } + return XRefActionNone, nil +} diff --git a/modules/references/references_test.go b/modules/references/references_test.go new file mode 100644 index 0000000000..f8153ffe36 --- /dev/null +++ b/modules/references/references_test.go @@ -0,0 +1,296 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package references + +import ( + "testing" + + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestFindAllIssueReferences(t *testing.T) { + + type result struct { + Index int64 + Owner string + Name string + Issue string + Action XRefAction + RefLocation *RefSpan + ActionLocation *RefSpan + } + + type testFixture struct { + input string + expected []result + } + + fixtures := []testFixture{ + { + "Simply closes: #29 yes", + []result{ + {29, "", "", "29", XRefActionCloses, &RefSpan{Start: 15, End: 18}, &RefSpan{Start: 7, End: 13}}, + }, + }, + { + "#123 no, this is a title.", + []result{}, + }, + { + " #124 yes, this is a reference.", + []result{ + {124, "", "", "124", XRefActionNone, &RefSpan{Start: 0, End: 4}, nil}, + }, + }, + { + "```\nThis is a code block.\n#723 no, it's a code block.```", + []result{}, + }, + { + "This `#724` no, it's inline code.", + []result{}, + }, + { + "This user3/repo4#200 yes.", + []result{ + {200, "user3", "repo4", "200", XRefActionNone, &RefSpan{Start: 5, End: 20}, nil}, + }, + }, + { + "This [one](#919) no, this is a URL fragment.", + []result{}, + }, + { + "This [two](/user2/repo1/issues/921) yes.", + []result{ + {921, "user2", "repo1", "921", XRefActionNone, nil, nil}, + }, + }, + { + "This [three](/user2/repo1/pulls/922) yes.", + []result{ + {922, "user2", "repo1", "922", XRefActionNone, nil, nil}, + }, + }, + { + "This [four](http://gitea.com:3000/user3/repo4/issues/203) yes.", + []result{ + {203, "user3", "repo4", "203", XRefActionNone, nil, nil}, + }, + }, + { + "This [five](http://github.com/user3/repo4/issues/204) no.", + []result{}, + }, + { + "This http://gitea.com:3000/user4/repo5/201 no, bad URL.", + []result{}, + }, + { + "This http://gitea.com:3000/user4/repo5/pulls/202 yes.", + []result{ + {202, "user4", "repo5", "202", XRefActionNone, nil, nil}, + }, + }, + { + "This http://GiTeA.COM:3000/user4/repo6/pulls/205 yes.", + []result{ + {205, "user4", "repo6", "205", XRefActionNone, nil, nil}, + }, + }, + { + "Reopens #15 yes", + []result{ + {15, "", "", "15", XRefActionReopens, &RefSpan{Start: 8, End: 11}, &RefSpan{Start: 0, End: 7}}, + }, + }, + { + "This closes #20 for you yes", + []result{ + {20, "", "", "20", XRefActionCloses, &RefSpan{Start: 12, End: 15}, &RefSpan{Start: 5, End: 11}}, + }, + }, + { + "Do you fix user6/repo6#300 ? yes", + []result{ + {300, "user6", "repo6", "300", XRefActionCloses, &RefSpan{Start: 11, End: 26}, &RefSpan{Start: 7, End: 10}}, + }, + }, + { + "For 999 #1235 no keyword, but yes", + []result{ + {1235, "", "", "1235", XRefActionNone, &RefSpan{Start: 8, End: 13}, nil}, + }, + }, + { + "Which abc. #9434 same as above", + []result{ + {9434, "", "", "9434", XRefActionNone, &RefSpan{Start: 11, End: 16}, nil}, + }, + }, + { + "This closes #600 and reopens #599", + []result{ + {600, "", "", "600", XRefActionCloses, &RefSpan{Start: 12, End: 16}, &RefSpan{Start: 5, End: 11}}, + {599, "", "", "599", XRefActionReopens, &RefSpan{Start: 29, End: 33}, &RefSpan{Start: 21, End: 28}}, + }, + }, + } + + // Save original value for other tests that may rely on it + prevURL := setting.AppURL + setting.AppURL = "https://gitea.com:3000/" + + for _, fixture := range fixtures { + expraw := make([]*rawReference, len(fixture.expected)) + for i, e := range fixture.expected { + expraw[i] = &rawReference{ + index: e.Index, + owner: e.Owner, + name: e.Name, + action: e.Action, + issue: e.Issue, + refLocation: e.RefLocation, + actionLocation: e.ActionLocation, + } + } + expref := rawToIssueReferenceList(expraw) + refs := FindAllIssueReferencesMarkdown(fixture.input) + assert.EqualValues(t, expref, refs, "Failed to parse: {%s}", fixture.input) + rawrefs := findAllIssueReferencesMarkdown(fixture.input) + assert.EqualValues(t, expraw, rawrefs, "Failed to parse: {%s}", fixture.input) + } + + // Restore for other tests that may rely on the original value + setting.AppURL = prevURL + + type alnumFixture struct { + input string + issue string + refLocation *RefSpan + action XRefAction + actionLocation *RefSpan + } + + alnumFixtures := []alnumFixture{ + { + "This ref ABC-123 is alphanumeric", + "ABC-123", &RefSpan{Start: 9, End: 16}, + XRefActionNone, nil, + }, + { + "This closes ABCD-1234 alphanumeric", + "ABCD-1234", &RefSpan{Start: 12, End: 21}, + XRefActionCloses, &RefSpan{Start: 5, End: 11}, + }, + } + + for _, fixture := range alnumFixtures { + found, ref := FindRenderizableReferenceAlphanumeric(fixture.input) + if fixture.issue == "" { + assert.False(t, found, "Failed to parse: {%s}", fixture.input) + } else { + assert.True(t, found, "Failed to parse: {%s}", fixture.input) + assert.Equal(t, fixture.issue, ref.Issue, "Failed to parse: {%s}", fixture.input) + assert.Equal(t, fixture.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input) + assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input) + assert.Equal(t, fixture.actionLocation, ref.ActionLocation, "Failed to parse: {%s}", fixture.input) + } + } +} + +func TestRegExp_mentionPattern(t *testing.T) { + trueTestCases := []string{ + "@Unknwon", + "@ANT_123", + "@xxx-DiN0-z-A..uru..s-xxx", + " @lol ", + " @Te-st", + "(@gitea)", + "[@gitea]", + } + falseTestCases := []string{ + "@ 0", + "@ ", + "@", + "", + "ABC", + "/home/gitea/@gitea", + "\"@gitea\"", + } + + for _, testCase := range trueTestCases { + res := mentionPattern.MatchString(testCase) + assert.True(t, res) + } + for _, testCase := range falseTestCases { + res := mentionPattern.MatchString(testCase) + assert.False(t, res) + } +} + +func TestRegExp_issueNumericPattern(t *testing.T) { + trueTestCases := []string{ + "#1234", + "#0", + "#1234567890987654321", + " #12", + "#12:", + "ref: #12: msg", + } + falseTestCases := []string{ + "# 1234", + "# 0", + "# ", + "#", + "#ABC", + "#1A2B", + "", + "ABC", + } + + for _, testCase := range trueTestCases { + assert.True(t, issueNumericPattern.MatchString(testCase)) + } + for _, testCase := range falseTestCases { + assert.False(t, issueNumericPattern.MatchString(testCase)) + } +} + +func TestRegExp_issueAlphanumericPattern(t *testing.T) { + trueTestCases := []string{ + "ABC-1234", + "A-1", + "RC-80", + "ABCDEFGHIJ-1234567890987654321234567890", + "ABC-123.", + "(ABC-123)", + "[ABC-123]", + "ABC-123:", + } + falseTestCases := []string{ + "RC-08", + "PR-0", + "ABCDEFGHIJK-1", + "PR_1", + "", + "#ABC", + "", + "ABC", + "GG-", + "rm-1", + "/home/gitea/ABC-1234", + "MY-STRING-ABC-123", + } + + for _, testCase := range trueTestCases { + assert.True(t, issueAlphanumericPattern.MatchString(testCase)) + } + for _, testCase := range falseTestCases { + assert.False(t, issueAlphanumericPattern.MatchString(testCase)) + } +} diff --git a/modules/repofiles/content.go b/modules/repofiles/content.go index 9637658e78..d7d43ef9d1 100644 --- a/modules/repofiles/content.go +++ b/modules/repofiles/content.go @@ -38,6 +38,9 @@ func (ct *ContentType) String() string { // GetContentsOrList gets the meta data of a file's contents (*ContentsResponse) if treePath not a tree // directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag func GetContentsOrList(repo *models.Repository, treePath, ref string) (interface{}, error) { + if repo.IsEmpty { + return make([]interface{}, 0), nil + } if ref == "" { ref = repo.DefaultBranch } diff --git a/modules/repofiles/content_test.go b/modules/repofiles/content_test.go index ef6c5eafc2..cd98c54ea6 100644 --- a/modules/repofiles/content_test.go +++ b/modules/repofiles/content_test.go @@ -190,3 +190,19 @@ func TestGetContentsOrListErrors(t *testing.T) { assert.Nil(t, fileContentResponse) }) } + +func TestGetContentsOrListOfEmptyRepos(t *testing.T) { + models.PrepareTestEnv(t) + ctx := test.MockContext(t, "user2/repo15") + ctx.SetParams(":id", "15") + test.LoadRepo(t, ctx, 15) + test.LoadUser(t, ctx, 2) + test.LoadGitRepo(t, ctx) + repo := ctx.Repo.Repository + + t.Run("empty repo", func(t *testing.T) { + contents, err := GetContentsOrList(repo, "", "") + assert.NoError(t, err) + assert.Empty(t, contents) + }) +} diff --git a/modules/repofiles/file_test.go b/modules/repofiles/file_test.go index 7c45139dd9..95ec175ed4 100644 --- a/modules/repofiles/file_test.go +++ b/modules/repofiles/file_test.go @@ -73,7 +73,7 @@ func getExpectedFileResponse() *api.FileResponse { }, Verification: &api.PayloadCommitVerification{ Verified: false, - Reason: "", + Reason: "gpg.error.not_signed_commit", Signature: "", Payload: "", }, diff --git a/modules/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go index f791c3cb96..b07d2a8973 100644 --- a/modules/repofiles/temp_repo.go +++ b/modules/repofiles/temp_repo.go @@ -21,6 +21,8 @@ import ( "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/gitdiff" + + "github.com/mcuadros/go-version" ) // TemporaryUploadRepository is a type to wrap our upload repositories as a shallow clone @@ -254,7 +256,11 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t authorSig := author.NewGitSig() committerSig := committer.NewGitSig() - // FIXME: Should we add SSH_ORIGINAL_COMMAND to this + binVersion, err := git.BinVersion() + if err != nil { + return "", fmt.Errorf("Unable to get git version: %v", err) + } + // Because this may call hooks we should pass in the environment env := append(os.Environ(), "GIT_AUTHOR_NAME="+authorSig.Name, @@ -264,11 +270,29 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t "GIT_COMMITTER_EMAIL="+committerSig.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) - commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute, + + messageBytes := new(bytes.Buffer) + _, _ = messageBytes.WriteString(message) + _, _ = messageBytes.WriteString("\n") + + args := []string{"commit-tree", treeHash, "-p", "HEAD"} + + // Determine if we should sign + if version.Compare(binVersion, "1.7.9", ">=") { + sign, keyID := t.repo.SignCRUDAction(author, t.basePath, "HEAD") + if sign { + args = append(args, "-S"+keyID) + } else if version.Compare(binVersion, "2.0.0", ">=") { + args = append(args, "--no-gpg-sign") + } + } + + commitHash, stderr, err := process.GetManager().ExecDirEnvStdIn(5*time.Minute, t.basePath, fmt.Sprintf("commitTree (git commit-tree): %s", t.basePath), env, - git.GitExecutable, "commit-tree", treeHash, "-p", "HEAD", "-m", message) + messageBytes, + git.GitExecutable, args...) if err != nil { return "", fmt.Errorf("git commit-tree: %s", stderr) } @@ -328,6 +352,12 @@ func (t *TemporaryUploadRepository) DiffIndex() (diff *gitdiff.Diff, err error) // CheckAttribute checks the given attribute of the provided files func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...string) (map[string]map[string]string, error) { + binVersion, err := git.BinVersion() + if err != nil { + log.Error("Error retrieving git version: %v", err) + return nil, err + } + stdOut := new(bytes.Buffer) stdErr := new(bytes.Buffer) @@ -335,7 +365,14 @@ func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...str ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - cmdArgs := []string{"check-attr", "-z", attribute, "--cached", "--"} + cmdArgs := []string{"check-attr", "-z", attribute} + + // git check-attr --cached first appears in git 1.7.8 + if version.Compare(binVersion, "1.7.8", ">=") { + cmdArgs = append(cmdArgs, "--cached") + } + cmdArgs = append(cmdArgs, "--") + for _, arg := range args { if arg != "" { cmdArgs = append(cmdArgs, arg) @@ -353,7 +390,7 @@ func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...str } pid := process.GetManager().Add(desc, cmd) - err := cmd.Wait() + err = cmd.Wait() process.GetManager().Remove(pid) if err != nil { diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index ee1b16bce9..8a1e51730b 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + pull_service "code.gitea.io/gitea/services/pull" stdcharset "golang.org/x/net/html/charset" "golang.org/x/text/transform" @@ -313,12 +314,6 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up } } - // Check there is no way this can return multiple infos - filename2attribute2info, err := t.CheckAttribute("filter", treePath) - if err != nil { - return nil, err - } - content := opts.Content if bom { content = string(charset.UTF8BOM) + content @@ -341,16 +336,23 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up opts.Content = content var lfsMetaObject *models.LFSMetaObject - if setting.LFS.StartServer && filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { - // OK so we are supposed to LFS this data! - oid, err := models.GenerateLFSOid(strings.NewReader(opts.Content)) + if setting.LFS.StartServer { + // Check there is no way this can return multiple infos + filename2attribute2info, err := t.CheckAttribute("filter", treePath) if err != nil { return nil, err } - lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(opts.Content)), RepositoryID: repo.ID} - content = lfsMetaObject.Pointer() - } + if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { + // OK so we are supposed to LFS this data! + oid, err := models.GenerateLFSOid(strings.NewReader(opts.Content)) + if err != nil { + return nil, err + } + lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(opts.Content)), RepositoryID: repo.ID} + content = lfsMetaObject.Pointer() + } + } // Add the object to the database objectHash, err := t.HashObject(strings.NewReader(content)) if err != nil { @@ -497,7 +499,7 @@ func PushUpdate(repo *models.Repository, branch string, opts models.PushUpdateOp log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) - go models.AddTestPullRequestTask(pusher, repo.ID, branch, true) + go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true) if opts.RefFullName == git.BranchPrefix+repo.DefaultBranch { models.UpdateRepoIndexer(repo) diff --git a/modules/repofiles/upload.go b/modules/repofiles/upload.go index f2ffec7ebc..202e66b89a 100644 --- a/modules/repofiles/upload.go +++ b/modules/repofiles/upload.go @@ -74,9 +74,12 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep infos[i] = uploadInfo{upload: upload} } - filename2attribute2info, err := t.CheckAttribute("filter", names...) - if err != nil { - return err + var filename2attribute2info map[string]map[string]string + if setting.LFS.StartServer { + filename2attribute2info, err = t.CheckAttribute("filter", names...) + if err != nil { + return err + } } // Copy uploaded files into repository. @@ -88,7 +91,7 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep defer file.Close() var objectHash string - if filename2attribute2info[uploadInfo.upload.Name] != nil && filename2attribute2info[uploadInfo.upload.Name]["filter"] == "lfs" { + if setting.LFS.StartServer && filename2attribute2info[uploadInfo.upload.Name] != nil && filename2attribute2info[uploadInfo.upload.Name]["filter"] == "lfs" { // Handle LFS // FIXME: Inefficient! this should probably happen in models.Upload oid, err := models.GenerateLFSOid(file) diff --git a/modules/repofiles/verification.go b/modules/repofiles/verification.go index 9fc084daaf..3889b7993c 100644 --- a/modules/repofiles/verification.go +++ b/modules/repofiles/verification.go @@ -18,10 +18,16 @@ func GetPayloadCommitVerification(commit *git.Commit) *structs.PayloadCommitVeri verification.Signature = commit.Signature.Signature verification.Payload = commit.Signature.Payload } - if verification.Reason != "" { - verification.Reason = commitVerification.Reason - } else if verification.Verified { - verification.Reason = "unsigned" + if commitVerification.SigningUser != nil { + verification.Signer = &structs.PayloadUser{ + Name: commitVerification.SigningUser.Name, + Email: commitVerification.SigningUser.Email, + } + } + verification.Verified = commitVerification.Verified + verification.Reason = commitVerification.Reason + if verification.Reason == "" && !verification.Verified { + verification.Reason = "gpg.error.not_signed_commit" } return verification } diff --git a/modules/setting/cron.go b/modules/setting/cron.go index c544c6c228..77f55168aa 100644 --- a/modules/setting/cron.go +++ b/modules/setting/cron.go @@ -49,6 +49,9 @@ var ( Schedule string OlderThan time.Duration } `ini:"cron.deleted_branches_cleanup"` + UpdateMigrationPosterID struct { + Schedule string + } `ini:"cron.update_migration_poster_id"` }{ UpdateMirror: struct { Enabled bool @@ -114,6 +117,11 @@ var ( Schedule: "@every 24h", OlderThan: 24 * time.Hour, }, + UpdateMigrationPosterID: struct { + Schedule string + }{ + Schedule: "@every 24h", + }, } ) diff --git a/modules/setting/database.go b/modules/setting/database.go index 2cac4824df..8c49ba3c5a 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -42,12 +42,11 @@ var ( DBConnectRetries int DBConnectBackoff time.Duration MaxIdleConns int + MaxOpenConns int ConnMaxLifetime time.Duration IterateBufferSize int }{ - Timeout: 500, - MaxIdleConns: 0, - ConnMaxLifetime: 3 * time.Second, + Timeout: 500, } ) @@ -80,8 +79,13 @@ func InitDBConfig() { Database.Charset = sec.Key("CHARSET").In("utf8", []string{"utf8", "utf8mb4"}) Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db")) Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) - Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(0) - Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFE_TIME").MustDuration(3 * time.Second) + Database.MaxIdleConns = sec.Key("MAX_IDLE_CONNS").MustInt(2) + if Database.UseMySQL { + Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFE_TIME").MustDuration(3 * time.Second) + } else { + Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFE_TIME").MustDuration(0) + } + Database.MaxOpenConns = sec.Key("MAX_OPEN_CONNS").MustInt(0) Database.IterateBufferSize = sec.Key("ITERATE_BUFFER_SIZE").MustInt(50) Database.LogSQL = sec.Key("LOG_SQL").MustBool(true) diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index 30c670d407..fbaef3fcf2 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -8,6 +8,7 @@ import ( "path" "path/filepath" "strings" + "time" "code.gitea.io/gitea/modules/log" @@ -34,6 +35,7 @@ var ( IssueQueueDir string IssueQueueConnStr string IssueQueueBatchNumber int + StartupTimeout time.Duration IncludePatterns []glob.Glob ExcludePatterns []glob.Glob }{ @@ -67,6 +69,7 @@ func newIndexerService() { Indexer.IssueQueueDir = sec.Key("ISSUE_INDEXER_QUEUE_DIR").MustString(path.Join(AppDataPath, "indexers/issues.queue")) Indexer.IssueQueueConnStr = sec.Key("ISSUE_INDEXER_QUEUE_CONN_STR").MustString(path.Join(AppDataPath, "")) Indexer.IssueQueueBatchNumber = sec.Key("ISSUE_INDEXER_QUEUE_BATCH_NUMBER").MustInt(20) + Indexer.StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(30 * time.Second) } // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing diff --git a/modules/setting/log.go b/modules/setting/log.go index 5e2d2d769d..cb8f142084 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -6,6 +6,7 @@ package setting import ( "encoding/json" + "fmt" golog "log" "os" "path" @@ -17,6 +18,8 @@ import ( ini "gopkg.in/ini.v1" ) +var filenameSuffix = "" + type defaultLogOptions struct { levelName string // LogLevel flags string @@ -112,7 +115,7 @@ func generateLogConfig(sec *ini.Section, name string, defaults defaultLogOptions panic(err.Error()) } - logConfig["filename"] = logPath + logConfig["filename"] = logPath + filenameSuffix logConfig["rotate"] = sec.Key("LOG_ROTATE").MustBool(true) logConfig["maxsize"] = 1 << uint(sec.Key("MAX_SIZE_SHIFT").MustInt(28)) logConfig["daily"] = sec.Key("DAILY_ROTATE").MustBool(true) @@ -277,6 +280,12 @@ func newLogService() { golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT))) } +// RestartLogsWithPIDSuffix restarts the logs with a PID suffix on files +func RestartLogsWithPIDSuffix() { + filenameSuffix = fmt.Sprintf(".%d", os.Getpid()) + NewLogServices(false) +} + // NewLogServices creates all the log services func NewLogServices(disableConsole bool) { newLogService() diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 728741576d..19c68d003f 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -65,6 +65,16 @@ var ( Issue struct { LockReasons []string } `ini:"repository.issue"` + + Signing struct { + SigningKey string + SigningName string + SigningEmail string + InitialCommit []string + CRUDActions []string `ini:"CRUD_ACTIONS"` + Merges []string + Wiki []string + } `ini:"repository.signing"` }{ AnsiCharset: "", ForcePrivate: false, @@ -122,6 +132,25 @@ var ( }{ LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","), }, + + // Signing settings + Signing: struct { + SigningKey string + SigningName string + SigningEmail string + InitialCommit []string + CRUDActions []string `ini:"CRUD_ACTIONS"` + Merges []string + Wiki []string + }{ + SigningKey: "default", + SigningName: "", + SigningEmail: "", + InitialCommit: []string{"always"}, + CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, + Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, + Wiki: []string{"never"}, + }, } RepoRootPath string ScriptType = "bash" diff --git a/modules/setting/service.go b/modules/setting/service.go index 905b1326f7..dea4081ee8 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -23,6 +23,7 @@ var Service struct { ShowRegistrationButton bool RequireSignInView bool EnableNotifyMail bool + EnableBasicAuth bool EnableReverseProxyAuth bool EnableReverseProxyAutoRegister bool EnableReverseProxyEmail bool @@ -60,6 +61,7 @@ func newService() { Service.EmailDomainWhitelist = sec.Key("EMAIL_DOMAIN_WHITELIST").Strings(",") Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration)) Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() + Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() Service.EnableReverseProxyEmail = sec.Key("ENABLE_REVERSE_PROXY_EMAIL").MustBool() diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 5e476854b2..f3dd45d7bf 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -87,6 +87,7 @@ var ( CertFile string KeyFile string StaticRootPath string + StaticCacheTime time.Duration EnableGzip bool LandingPageURL LandingPage UnixSocketPermission uint32 @@ -96,6 +97,9 @@ var ( LetsEncryptTOS bool LetsEncryptDirectory string LetsEncryptEmail string + GracefulRestartable bool + GracefulHammerTime time.Duration + StaticURLPrefix string SSH = struct { Disabled bool `ini:"DISABLE_SSH"` @@ -146,6 +150,7 @@ var ( MinPasswordLength int ImportLocalPaths bool DisableGitHooks bool + PasswordComplexity []string PasswordHashAlgo string // UI settings @@ -515,7 +520,7 @@ func NewContext() { } else { log.Warn("Custom config '%s' not found, ignore this if you're running first time", CustomConf) } - Cfg.NameMapper = ini.AllCapsUnderscore + Cfg.NameMapper = ini.SnackCase homeDir, err := com.HomeDir() if err != nil { @@ -561,13 +566,15 @@ func NewContext() { Domain = sec.Key("DOMAIN").MustString("localhost") HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0") HTTPPort = sec.Key("HTTP_PORT").MustString("3000") + GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true) + GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second) defaultAppURL := string(Protocol) + "://" + Domain if (Protocol == HTTP && HTTPPort != "80") || (Protocol == HTTPS && HTTPPort != "443") { defaultAppURL += ":" + HTTPPort } AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL) - AppURL = strings.TrimRight(AppURL, "/") + "/" + AppURL = strings.TrimSuffix(AppURL, "/") + "/" // Check if has app suburl. appURL, err := url.Parse(AppURL) @@ -577,6 +584,7 @@ func NewContext() { // Suburl should start with '/' and end without '/', such as '/{subpath}'. // This value is empty if site does not have sub-url. AppSubURL = strings.TrimSuffix(appURL.Path, "/") + StaticURLPrefix = strings.TrimSuffix(sec.Key("STATIC_URL_PREFIX").MustString(AppSubURL), "/") AppSubURLDepth = strings.Count(AppSubURL, "/") // Check if Domain differs from AppURL domain than update it to AppURL's domain // TODO: Can be replaced with url.Hostname() when minimal GoLang version is 1.8 @@ -606,6 +614,7 @@ func NewContext() { OfflineMode = sec.Key("OFFLINE_MODE").MustBool() DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool() StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(AppWorkPath) + StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) EnableGzip = sec.Key("ENABLE_GZIP").MustBool() EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false) @@ -774,6 +783,15 @@ func NewContext() { InternalToken = loadInternalToken(sec) + cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",") + PasswordComplexity = make([]string, 0, len(cfgdata)) + for _, name := range cfgdata { + name := strings.ToLower(strings.Trim(name, `"`)) + if name != "" { + PasswordComplexity = append(PasswordComplexity, name) + } + } + sec = Cfg.Section("attachment") AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments")) if !filepath.IsAbs(AttachmentPath) { @@ -1043,4 +1061,5 @@ func NewServices() { newNotifyMailService() newWebhookService() newIndexerService() + newTaskService() } diff --git a/modules/setting/task.go b/modules/setting/task.go new file mode 100644 index 0000000000..97704d4a4d --- /dev/null +++ b/modules/setting/task.go @@ -0,0 +1,25 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package setting + +var ( + // Task settings + Task = struct { + QueueType string + QueueLength int + QueueConnStr string + }{ + QueueType: ChannelQueueType, + QueueLength: 1000, + QueueConnStr: "addrs=127.0.0.1:6379 db=0", + } +) + +func newTaskService() { + sec := Cfg.Section("task") + Task.QueueType = sec.Key("QUEUE_TYPE").MustString(ChannelQueueType) + Task.QueueLength = sec.Key("QUEUE_LENGTH").MustInt(1000) + Task.QueueConnStr = sec.Key("QUEUE_CONN_STR").MustString("addrs=127.0.0.1:6379 db=0") +} diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index 7ff0c32326..e7a694683a 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -183,12 +183,7 @@ func Listen(host string, port int, ciphers []string, keyExchanges []string, macs log.Error("Failed to set Host Key. %s", err) } - go func() { - err := srv.ListenAndServe() - if err != nil { - log.Error("Failed to serve with builtin SSH server. %s", err) - } - }() + go listen(&srv) } diff --git a/modules/ssh/ssh_graceful.go b/modules/ssh/ssh_graceful.go new file mode 100644 index 0000000000..d66c7d6540 --- /dev/null +++ b/modules/ssh/ssh_graceful.go @@ -0,0 +1,30 @@ +// +build !windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package ssh + +import ( + "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" + + "github.com/gliderlabs/ssh" +) + +func listen(server *ssh.Server) { + gracefulServer := graceful.NewServer("tcp", server.Addr) + + err := gracefulServer.ListenAndServe(server.Serve) + if err != nil { + log.Critical("Failed to start SSH server: %v", err) + } + log.Info("SSH Listener: %s Closed", server.Addr) + +} + +// Unused informs our cleanup routine that we will not be using a ssh port +func Unused() { + graceful.InformCleanup() +} diff --git a/modules/ssh/ssh_windows.go b/modules/ssh/ssh_windows.go new file mode 100644 index 0000000000..55032e17cd --- /dev/null +++ b/modules/ssh/ssh_windows.go @@ -0,0 +1,24 @@ +// +build windows + +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package ssh + +import ( + "code.gitea.io/gitea/modules/log" + "github.com/gliderlabs/ssh" +) + +func listen(server *ssh.Server) { + err := server.ListenAndServe() + if err != nil { + log.Critical("Failed to serve with builtin SSH server. %s", err) + } +} + +// Unused does nothing on windows +func Unused() { + // Do nothing +} diff --git a/modules/structs/hook.go b/modules/structs/hook.go index 9a25219e36..e036442904 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -91,10 +91,11 @@ type PayloadCommit struct { // PayloadCommitVerification represents the GPG verification of a commit type PayloadCommitVerification struct { - Verified bool `json:"verified"` - Reason string `json:"reason"` - Signature string `json:"signature"` - Payload string `json:"payload"` + Verified bool `json:"verified"` + Reason string `json:"reason"` + Signature string `json:"signature"` + Signer *PayloadUser `json:"signer"` + Payload string `json:"payload"` } var ( @@ -235,6 +236,7 @@ type IssueCommentPayload struct { Changes *ChangesPayload `json:"changes,omitempty"` Repository *Repository `json:"repository"` Sender *User `json:"sender"` + IsPull bool `json:"is_pull"` } // SetSecret modifies the secret of the IssueCommentPayload @@ -418,6 +420,7 @@ type PullRequestPayload struct { PullRequest *PullRequest `json:"pull_request"` Repository *Repository `json:"repository"` Sender *User `json:"sender"` + Review *ReviewPayload `json:"review"` } // SetSecret modifies the secret of the PullRequestPayload. @@ -430,6 +433,12 @@ func (p *PullRequestPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } +// ReviewPayload FIXME +type ReviewPayload struct { + Type string `json:"type"` + Content string `json:"content"` +} + //__________ .__ __ //\______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. // | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 87396d6ce9..be6a3d4b43 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -153,6 +153,43 @@ type EditRepoOption struct { Archived *bool `json:"archived,omitempty"` } +// GitServiceType represents a git service +type GitServiceType int + +// enumerate all GitServiceType +const ( + NotMigrated GitServiceType = iota // 0 not migrated from external sites + PlainGitService // 1 plain git service + GithubService // 2 github.com + GiteaService // 3 gitea service + GitlabService // 4 gitlab service + GogsService // 5 gogs service +) + +// Name represents the service type's name +// WARNNING: the name have to be equal to that on goth's library +func (gt GitServiceType) Name() string { + switch gt { + case GithubService: + return "github" + case GiteaService: + return "gitea" + case GitlabService: + return "gitlab" + case GogsService: + return "gogs" + } + return "" +} + +var ( + // SupportedFullGitService represents all git services supported to migrate issues/labels/prs and etc. + // TODO: add to this list after new git service added + SupportedFullGitService = []GitServiceType{ + GithubService, + } +) + // MigrateRepoOption options for migrating a repository from an external service type MigrateRepoOption struct { // required: true @@ -162,8 +199,18 @@ type MigrateRepoOption struct { // required: true UID int `json:"uid" binding:"Required"` // required: true - RepoName string `json:"repo_name" binding:"Required"` - Mirror bool `json:"mirror"` - Private bool `json:"private"` - Description string `json:"description"` + RepoName string `json:"repo_name" binding:"Required"` + Mirror bool `json:"mirror"` + Private bool `json:"private"` + Description string `json:"description"` + OriginalURL string + GitServiceType GitServiceType + Wiki bool + Issues bool + Milestones bool + Labels bool + Releases bool + Comments bool + PullRequests bool + MigrateToRepoID int64 } diff --git a/modules/structs/task.go b/modules/structs/task.go new file mode 100644 index 0000000000..e83d0437ce --- /dev/null +++ b/modules/structs/task.go @@ -0,0 +1,34 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package structs + +// TaskType defines task type +type TaskType int + +// all kinds of task types +const ( + TaskTypeMigrateRepo TaskType = iota // migrate repository from external or local disk +) + +// Name returns the task type name +func (taskType TaskType) Name() string { + switch taskType { + case TaskTypeMigrateRepo: + return "Migrate Repository" + } + return "" +} + +// TaskStatus defines task status +type TaskStatus int + +// enumerate all the kinds of task status +const ( + TaskStatusQueue TaskStatus = iota // 0 task is queue + TaskStatusRunning // 1 task is running + TaskStatusStopped // 2 task is stopped + TaskStatusFailed // 3 task is failed + TaskStatusFinished // 4 task is finished +) diff --git a/modules/task/migrate.go b/modules/task/migrate.go new file mode 100644 index 0000000000..5d15a506d7 --- /dev/null +++ b/modules/task/migrate.go @@ -0,0 +1,120 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migrations" + "code.gitea.io/gitea/modules/notification" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" +) + +func handleCreateError(owner *models.User, err error, name string) error { + switch { + case models.IsErrReachLimitOfRepo(err): + return fmt.Errorf("You have already reached your limit of %d repositories", owner.MaxCreationLimit()) + case models.IsErrRepoAlreadyExist(err): + return errors.New("The repository name is already used") + case models.IsErrNameReserved(err): + return fmt.Errorf("The repository name '%s' is reserved", err.(models.ErrNameReserved).Name) + case models.IsErrNamePatternNotAllowed(err): + return fmt.Errorf("The pattern '%s' is not allowed in a repository name", err.(models.ErrNamePatternNotAllowed).Pattern) + default: + return err + } +} + +func runMigrateTask(t *models.Task) (err error) { + defer func() { + if e := recover(); e != nil { + var buf bytes.Buffer + fmt.Fprintf(&buf, "Handler crashed with error: %v", log.Stack(2)) + + err = errors.New(buf.String()) + } + + if err == nil { + err = models.FinishMigrateTask(t) + if err == nil { + notification.NotifyMigrateRepository(t.Doer, t.Owner, t.Repo) + return + } + + log.Error("FinishMigrateTask failed: %s", err.Error()) + } + + t.EndTime = timeutil.TimeStampNow() + t.Status = structs.TaskStatusFailed + t.Errors = err.Error() + if err := t.UpdateCols("status", "errors", "end_time"); err != nil { + log.Error("Task UpdateCols failed: %s", err.Error()) + } + + if t.Repo != nil { + if errDelete := models.DeleteRepository(t.Doer, t.OwnerID, t.Repo.ID); errDelete != nil { + log.Error("DeleteRepository: %v", errDelete) + } + } + }() + + if err := t.LoadRepo(); err != nil { + return err + } + + // if repository is ready, then just finsih the task + if t.Repo.Status == models.RepositoryReady { + return nil + } + + if err := t.LoadDoer(); err != nil { + return err + } + if err := t.LoadOwner(); err != nil { + return err + } + t.StartTime = timeutil.TimeStampNow() + t.Status = structs.TaskStatusRunning + if err := t.UpdateCols("start_time", "status"); err != nil { + return err + } + + var opts *structs.MigrateRepoOption + opts, err = t.MigrateConfig() + if err != nil { + return err + } + + opts.MigrateToRepoID = t.RepoID + repo, err := migrations.MigrateRepository(t.Doer, t.Owner.Name, *opts) + if err == nil { + notification.NotifyMigrateRepository(t.Doer, t.Owner, repo) + + log.Trace("Repository migrated [%d]: %s/%s", repo.ID, t.Owner.Name, repo.Name) + return nil + } + + if models.IsErrRepoAlreadyExist(err) { + return errors.New("The repository name is already used") + } + + // remoteAddr may contain credentials, so we sanitize it + err = util.URLSanitizedError(err, opts.CloneAddr) + if strings.Contains(err.Error(), "Authentication failed") || + strings.Contains(err.Error(), "could not read Username") { + return fmt.Errorf("Authentication failed: %v", err.Error()) + } else if strings.Contains(err.Error(), "fatal:") { + return fmt.Errorf("Migration failed: %v", err.Error()) + } + + return handleCreateError(t.Owner, err, "MigratePost") +} diff --git a/modules/task/queue.go b/modules/task/queue.go new file mode 100644 index 0000000000..ddee0b3d46 --- /dev/null +++ b/modules/task/queue.go @@ -0,0 +1,14 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import "code.gitea.io/gitea/models" + +// Queue defines an interface to run task queue +type Queue interface { + Run() error + Push(*models.Task) error + Stop() +} diff --git a/modules/task/queue_channel.go b/modules/task/queue_channel.go new file mode 100644 index 0000000000..da541f4755 --- /dev/null +++ b/modules/task/queue_channel.go @@ -0,0 +1,48 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" +) + +var ( + _ Queue = &ChannelQueue{} +) + +// ChannelQueue implements +type ChannelQueue struct { + queue chan *models.Task +} + +// NewChannelQueue create a memory channel queue +func NewChannelQueue(queueLen int) *ChannelQueue { + return &ChannelQueue{ + queue: make(chan *models.Task, queueLen), + } +} + +// Run starts to run the queue +func (c *ChannelQueue) Run() error { + for task := range c.queue { + err := Run(task) + if err != nil { + log.Error("Run task failed: %s", err.Error()) + } + } + return nil +} + +// Push will push the task ID to queue +func (c *ChannelQueue) Push(task *models.Task) error { + c.queue <- task + return nil +} + +// Stop stop the queue +func (c *ChannelQueue) Stop() { + close(c.queue) +} diff --git a/modules/task/queue_redis.go b/modules/task/queue_redis.go new file mode 100644 index 0000000000..127de0cdbf --- /dev/null +++ b/modules/task/queue_redis.go @@ -0,0 +1,130 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import ( + "encoding/json" + "errors" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + + "github.com/go-redis/redis" +) + +var ( + _ Queue = &RedisQueue{} +) + +type redisClient interface { + RPush(key string, args ...interface{}) *redis.IntCmd + LPop(key string) *redis.StringCmd + Ping() *redis.StatusCmd +} + +// RedisQueue redis queue +type RedisQueue struct { + client redisClient + queueName string + closeChan chan bool +} + +func parseConnStr(connStr string) (addrs, password string, dbIdx int, err error) { + fields := strings.Fields(connStr) + for _, f := range fields { + items := strings.SplitN(f, "=", 2) + if len(items) < 2 { + continue + } + switch strings.ToLower(items[0]) { + case "addrs": + addrs = items[1] + case "password": + password = items[1] + case "db": + dbIdx, err = strconv.Atoi(items[1]) + if err != nil { + return + } + } + } + return +} + +// NewRedisQueue creates single redis or cluster redis queue +func NewRedisQueue(addrs string, password string, dbIdx int) (*RedisQueue, error) { + dbs := strings.Split(addrs, ",") + var queue = RedisQueue{ + queueName: "task_queue", + closeChan: make(chan bool), + } + if len(dbs) == 0 { + return nil, errors.New("no redis host found") + } else if len(dbs) == 1 { + queue.client = redis.NewClient(&redis.Options{ + Addr: strings.TrimSpace(dbs[0]), // use default Addr + Password: password, // no password set + DB: dbIdx, // use default DB + }) + } else { + // cluster will ignore db + queue.client = redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: dbs, + Password: password, + }) + } + if err := queue.client.Ping().Err(); err != nil { + return nil, err + } + return &queue, nil +} + +// Run starts to run the queue +func (r *RedisQueue) Run() error { + for { + select { + case <-r.closeChan: + return nil + case <-time.After(time.Millisecond * 100): + } + + bs, err := r.client.LPop(r.queueName).Bytes() + if err != nil { + if err != redis.Nil { + log.Error("LPop failed: %v", err) + } + time.Sleep(time.Millisecond * 100) + continue + } + + var task models.Task + err = json.Unmarshal(bs, &task) + if err != nil { + log.Error("Unmarshal task failed: %s", err.Error()) + } else { + err = Run(&task) + if err != nil { + log.Error("Run task failed: %s", err.Error()) + } + } + } +} + +// Push implements Queue +func (r *RedisQueue) Push(task *models.Task) error { + bs, err := json.Marshal(task) + if err != nil { + return err + } + return r.client.RPush(r.queueName, bs).Err() +} + +// Stop stop the queue +func (r *RedisQueue) Stop() { + r.closeChan <- true +} diff --git a/modules/task/task.go b/modules/task/task.go new file mode 100644 index 0000000000..64744afe7a --- /dev/null +++ b/modules/task/task.go @@ -0,0 +1,66 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import ( + "fmt" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" +) + +// taskQueue is a global queue of tasks +var taskQueue Queue + +// Run a task +func Run(t *models.Task) error { + switch t.Type { + case structs.TaskTypeMigrateRepo: + return runMigrateTask(t) + default: + return fmt.Errorf("Unknow task type: %d", t.Type) + } +} + +// Init will start the service to get all unfinished tasks and run them +func Init() error { + switch setting.Task.QueueType { + case setting.ChannelQueueType: + taskQueue = NewChannelQueue(setting.Task.QueueLength) + case setting.RedisQueueType: + var err error + addrs, pass, idx, err := parseConnStr(setting.Task.QueueConnStr) + if err != nil { + return err + } + taskQueue, err = NewRedisQueue(addrs, pass, idx) + if err != nil { + return err + } + default: + return fmt.Errorf("Unsupported task queue type: %v", setting.Task.QueueType) + } + + go func() { + if err := taskQueue.Run(); err != nil { + log.Error("taskQueue.Run end failed: %v", err) + } + }() + + return nil +} + +// MigrateRepository add migration repository to task +func MigrateRepository(doer, u *models.User, opts base.MigrateOptions) error { + task, err := models.CreateMigrateTask(doer, u, opts) + if err != nil { + return err + } + + return taskQueue.Push(task) +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index b40f7117f5..b5287bf971 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -30,7 +30,7 @@ import ( "code.gitea.io/gitea/services/gitdiff" mirror_service "code.gitea.io/gitea/services/mirror" - "gopkg.in/editorconfig/editorconfig-core-go.v1" + "github.com/editorconfig/editorconfig-core-go/v2" ) // NewFuncMap returns functions for injecting to templates @@ -48,6 +48,9 @@ func NewFuncMap() []template.FuncMap { "AppSubUrl": func() string { return setting.AppSubURL }, + "StaticUrlPrefix": func() string { + return setting.StaticURLPrefix + }, "AppUrl": func() string { return setting.AppURL }, @@ -145,7 +148,11 @@ func NewFuncMap() []template.FuncMap { }, "TabSizeClass": func(ec *editorconfig.Editorconfig, filename string) string { if ec != nil { - def := ec.GetDefinitionForFilename(filename) + def, err := ec.GetDefinitionForFilename(filename) + if err != nil { + log.Error("tab size class: getting definition for filename: %v", err) + return "tab-size-8" + } if def.TabWidth > 0 { return fmt.Sprintf("tab-size-%d", def.TabWidth) } @@ -236,6 +243,8 @@ func NewFuncMap() []template.FuncMap { "CommentMustAsDiff": gitdiff.CommentMustAsDiff, "MirrorAddress": mirror_service.Address, "MirrorFullAddress": mirror_service.AddressNoCredentials, + "MirrorUserName": mirror_service.Username, + "MirrorPassword": mirror_service.Password, }} } diff --git a/modules/util/compare.go b/modules/util/compare.go index c61e7965ae..f1d1e5718e 100644 --- a/modules/util/compare.go +++ b/modules/util/compare.go @@ -35,6 +35,16 @@ func ExistsInSlice(target string, slice []string) bool { return i < len(slice) } +// IsStringInSlice sequential searches if string exists in slice. +func IsStringInSlice(target string, slice []string) bool { + for i := 0; i < len(slice); i++ { + if slice[i] == target { + return true + } + } + return false +} + // IsEqualSlice returns true if slices are equal. func IsEqualSlice(target []string, source []string) bool { if len(target) != len(source) { diff --git a/options/locale/TRANSLATORS b/options/locale/TRANSLATORS index c413626ec1..98a47a6c53 100644 --- a/options/locale/TRANSLATORS +++ b/options/locale/TRANSLATORS @@ -73,5 +73,6 @@ Toni Villena Jiménez Viktor Sperl Vladimir Jigulin mogaika AT yandex DOT ru Vladimir Vissoultchev +Yaşar Çiv YJSoft Łukasz Jan Niemier diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index 48c007b60d..40dabfae9f 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -1,4 +1,3 @@ - home=Начало dashboard=Табло explore=Каталог @@ -40,6 +39,8 @@ issues=Задачи cancel=Отказ +[startpage] + [install] install=Инсталация db_title=Настройки на базата данни @@ -472,7 +473,6 @@ settings.choose_branch=Изберете клон… diff.browse_source=Преглед на файлове diff.parent=родител diff.commit=ревизия -diff.show_diff_stats=Покажи статистика за разликите diff.show_split_view=Разделен изглед diff.show_unified_view=Обединен изглед diff.stats_desc=променени са %d файла, в които са добавени %d реда и са изтрити %d реда diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index a103f423d9..24680cea5b 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1,5 +1,3 @@ -app_desc=Snadno přístupný vlastní Git - home=Domů dashboard=Přehled explore=Procházet @@ -75,6 +73,8 @@ write=Zapsat preview=Náhled loading=Načítá se… +[startpage] + [install] install=Instalace title=Výchozí konfigurace @@ -250,7 +250,6 @@ openid_signin_desc=Zadejte své OpenID URI. Například: https://anne.me, bob.op disable_forgot_password_mail=Obnovení účtu je zakázané. Prosíme, kontaktujte správce systému. email_domain_blacklisted=Nemůžete se registrovat s vaší e-mailovou adresou. authorize_application=Autorizovat aplikaci -authroize_redirect_notice=Budete přesměrováni na %s, pokud autorizujete tuto aplikaci. authorize_application_created_by=Tuto aplikaci vytvořil %s. authorize_application_description=Pokud povolíte přístup, bude moci přistupovat a zapisovat do všech vašich informací o účtu včetně soukromých repozitářů a organizací. authorize_title=Autorizovat „%s“ pro přístup k vašemu účtu? @@ -1357,7 +1356,6 @@ diff.parent=rodič diff.commit=revize diff.git-notes=Poznámky diff.data_not_available=Rozdílový obsah není dostupný -diff.show_diff_stats=Ukázat statistiku rozdílových dat diff.show_split_view=Rozdělené zobrazení diff.show_unified_view=Jednotný pohled diff.whitespace_button=Bílý znak diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 3add4be1a9..b692e26c06 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1,5 +1,3 @@ -app_desc=Ein einfacher, selbst gehosteter Git-Service - home=Startseite dashboard=Übersicht explore=Erkunden @@ -52,7 +50,7 @@ new_mirror=Neuer Mirror new_fork=Neuer Fork new_org=Neue Organisation manage_org=Organisationen verwalten -admin_panel=Website-Administration +admin_panel=Administration account_settings=Kontoeinstellungen settings=Einstellungen your_profile=Profil @@ -75,6 +73,17 @@ write=Verfassen preview=Vorschau loading=Laden… +[startpage] +app_desc=Ein einfacher, selbst gehosteter Git-Service +install=Einfach zu installieren +install_desc=Starte einfach die Anwendung für deine Plattform. Gitea gibt es auch für Docker, Vagrant oder als Installationspaket. +platform=Plattformübergreifend +platform_desc=Gitea läuft überall, wo Go kompiliert: Windows, macOS, Linux, ARM, etc. Wähle das System, das dir am meisten gefällt! +lightweight=Leichtgewicht +lightweight_desc=Gitea hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden! +license=Quelloffen +license_desc=Der komplette Code befindet sich auf GitHub! Unterstütze uns bei der Verbesserung dieses Projekts. Trau dich! + [install] install=Installation title=Erstkonfiguration @@ -250,7 +259,6 @@ openid_signin_desc=Gib deine OpenID-URI ein. Zum Beispiel: https://anne.me, bob. disable_forgot_password_mail=Die Kontowiederherstellung ist deaktiviert. Bitte wende dich an den Administrator. email_domain_blacklisted=Du kannst dich nicht mit deiner E-Mail-Adresse registrieren. authorize_application=Anwendung autorisieren -authroize_redirect_notice=Wenn du diese Anwendung autorisierst, wirst du zu %s weitergeleitet. authorize_application_created_by=Diese Anwendung wurde von %s erstellt. authorize_application_description=Wenn du diese Anwendung autorisierst, wird sie die Berechtigung erhalten, alle Informationen zu deinem Account zu bearbeiten oder zu lesen. Dies beinhaltet auch private Repositories und Organisationen. authorize_title="%s" den Zugriff auf deinen Account gestatten? @@ -314,10 +322,12 @@ team_no_units_error=Das Team muss auf mindestens einen Bereich Zugriff haben. email_been_used=Die E-Mail-Adresse wird bereits verwendet. openid_been_used=Die OpenID-Adresse „%s“ wird bereits verwendet. username_password_incorrect=Benutzername oder Passwort ist falsch. +password_complexity=Das Passwort erfüllt nicht die Komplexitätsanforderungen. enterred_invalid_repo_name=Der eingegebenen Repository-Name ist falsch. enterred_invalid_owner_name=Der Name des neuen Besitzers ist ungültig. enterred_invalid_password=Das eingegebene Passwort ist falsch. user_not_exist=Dieser Benutzer ist nicht vorhanden. +team_not_exist=Dieses Team existiert nicht. last_org_owner=Du kannst den letzten Benutzer nicht aus dem „Besitzer“-Team entfernen. Es muss mindestens einen Besitzer in einer Organisation geben. cannot_add_org_to_team=Eine Organisation kann nicht als Teammitglied hinzugefügt werden. @@ -631,6 +641,8 @@ migrate.lfs_mirror_unsupported=Spiegeln von LFS-Objekten wird nicht unterstützt migrate.migrate_items_options=Wenn du von GitHub migrierst und einen Benutzernamen eingegeben hast, werden die Migrationsoptionen angezeigt. migrated_from=Migriert von %[2]s migrated_from_fake=Migriert von %[1]s +migrate.migrating=Migriere von %s ... +migrate.migrating_failed=Migrieren von %s fehlgeschlagen. mirror_from=Mirror von forked_from=geforkt von @@ -679,6 +691,8 @@ stored_lfs=Gespeichert mit Git LFS commit_graph=Commit graph blame=Blame normal_view=Normale Ansicht +line=zeile +lines=Zeilen editor.new_file=Neue Datei editor.upload_file=Datei hochladen @@ -704,6 +718,7 @@ editor.delete=„%s“ löschen editor.commit_message_desc=Eine ausführlichere (optionale) Beschreibung hinzufügen… editor.commit_directly_to_this_branch=Direkt in den Branch „%s“ einchecken. editor.create_new_branch=Einen neuen Branch für diesen Commit erstellen und einen Pull Request starten. +editor.create_new_branch_np=Erstelle einen neuen Branch für diesen Commit. editor.propose_file_change=Dateiänderung vorschlagen editor.new_branch_name_desc=Neuer Branchname… editor.cancel=Abbrechen @@ -718,6 +733,8 @@ editor.file_editing_no_longer_exists=Die bearbeitete Datei „%s“ existiert ni editor.file_deleting_no_longer_exists=Die Datei '%s' existiert in diesem Repository nicht mehr. editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. Hier klicken, um die Änderungen anzusehen, oder Änderungen erneut comitten, um sie zu überschreiben. editor.file_already_exists=Eine Datei mit dem Namen „%s“ ist bereits in diesem Repository vorhanden. +editor.commit_empty_file_header=Leere Datei committen +editor.commit_empty_file_text=Die Datei, die du gerade commitest ist leer! Fortfahren? editor.no_changes_to_show=Keine Änderungen vorhanden. editor.fail_to_update_file=Fehler beim Ändern/Erstellen der Datei „%s“. Fehler: %v editor.add_subdir=Verzeichnis erstellen… @@ -835,6 +852,10 @@ issues.create_comment=Kommentieren issues.closed_at=`hat %[2]s geschlossen` issues.reopened_at=`hat %[2]s wieder geöffnet` issues.commit_ref_at=`hat dieses Issue %[2]s aus einem Commit referenziert` +issues.ref_issue_at=`hat dieses Issue %[1]s referenziert` +issues.ref_pull_at=`hat diesen Pull-Request %[1]s referenziert` +issues.ref_issue_ext_at=`hat dieses Issue %[2]s von %[1]s referenziert` +issues.ref_pull_ext_at=`hat diesen Pull-Request %[2]s von %[1]s referenziert` issues.poster=Ersteller issues.collaborator=Mitarbeiter issues.owner=Besitzer @@ -1131,6 +1152,7 @@ settings.collaboration=Mitarbeiter settings.collaboration.admin=Administrator settings.collaboration.write=Schreibrechte settings.collaboration.read=Leserechte +settings.collaboration.owner=Besitzer settings.collaboration.undefined=Nicht definiert settings.hooks=Webhooks settings.githooks=Git-Hooks @@ -1212,6 +1234,11 @@ settings.collaborator_deletion_desc=Nach dem Löschen wird dieser Mitarbeiter ke settings.remove_collaborator_success=Der Mitarbeiter wurde entfernt. settings.search_user_placeholder=Benutzer suchen… settings.org_not_allowed_to_be_collaborator=Organisationen können nicht als Mitarbeiter hinzugefügt werden. +settings.change_team_access_not_allowed=Nur der Besitzer der Organisation kann die Zugangsrechte des Teams ändern +settings.team_not_in_organization=Das Team ist nicht in der gleichen Organisation wie das Repository +settings.add_team_duplicate=Das Team ist dem Repository schon zugeordnet +settings.add_team_success=Das Team hat nun Zugriff auf das Repository. +settings.remove_team_success=Der Zugriff des Teams auf das Repository wurde zurückgezogen. settings.add_webhook=Webhook hinzufügen settings.add_webhook.invalid_channel_name=Der Name des Webhook-Kanals darf nicht leer sein und darf nicht nur das Zeichen # enthalten. settings.hooks_desc=Webhooks senden bei bestimmten Gitea-Events automatisch „HTTP POST“-Anfragen an einen Server. Lies mehr in unserer Anleitung zu Webhooks (auf Englisch). @@ -1314,6 +1341,7 @@ settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen: settings.protect_merge_whitelist_teams=Teams, die mergen dürfen: settings.protect_check_status_contexts=Statusprüfungen aktivieren +settings.protect_check_status_contexts_desc=Statusprüfungen vor dem Zusammenführen erforderlich. Wähle aus, welche Statusprüfungen durchgeführt werden müssen, bevor Branches zu einem Zweig zusammengeführt werden können, der dieser Regel entspricht. Wenn aktiviert, müssen Bestätigungen zuerst auf einen anderen Zweig verschoben werden, dann nach bestandener Statusprüfung zusammengeführt oder direkt auf einen Zweig verschoben werden, der dieser Regel entspricht. Wenn keine Kontext ausgewählt ist, muss der letzte Commit unabhängig vom Kontext erfolgreich sein. settings.protect_check_status_contexts_list=Statusprüfungen, die in der letzten Woche für dieses Repository gefunden wurden settings.protect_required_approvals=Erforderliche Zustimmungen: settings.protect_required_approvals_desc=Erlaube das Zusammenführen des Pull-Requests nur bei ausreichend positiven Zustimmungen von dafür freigeschalteten Nutzern oder Teams. @@ -1351,7 +1379,10 @@ diff.parent=Ursprung diff.commit=Commit diff.git-notes=Hinweise diff.data_not_available=Keine Diff-Daten verfügbar -diff.show_diff_stats=Diff-Statistik anzeigen +diff.options_button=Diff-Optionen +diff.show_diff_stats=Statistiken anzeigen +diff.download_patch=Patch-Datei herunterladen +diff.download_diff=Vergleichs-Datei herunterladen diff.show_split_view=Geteilte Ansicht diff.show_unified_view=Gesamtansicht diff.whitespace_button=Leerzeichen @@ -1434,6 +1465,8 @@ branch.restore_failed=Wiederherstellung des Branches „%s“ fehlgeschlagen. branch.protected_deletion_failed=Branch „%s“ ist geschützt und kann nicht gelöscht werden. branch.restore=Branch „%s“ wiederherstellen branch.download=Branch „%s“ herunterladen +branch.included_desc=Dieser Branch ist im Standard-Branch enthalten +branch.included=Enthalten topic.manage_topics=Themen verwalten topic.done=Fertig @@ -1469,6 +1502,8 @@ settings.options=Organisation settings.full_name=Vollständiger Name settings.website=Webseite settings.location=Standort +settings.permission=Berechtigungen +settings.repoadminchangeteam=Der Repository-Administrator kann den Zugriff für Teams hinzufügen und zurückziehen settings.visibility=Sichtbarkeit settings.visibility.public=Öffentlich settings.visibility.limited=Eingeschränkt (nur für angemeldete Nutzer sichtbar) @@ -1948,12 +1983,15 @@ mark_as_unread=Als ungelesen markieren mark_all_as_read=Alle als gelesen markieren [gpg] +default_key=Mit Standardschlüssel signiert error.extract_sign=Die Signatur konnte nicht extrahiert werden error.generate_hash=Es konnte kein Hash vom Commit generiert werden error.no_committer_account=Es ist kein Account mit der E-Mail-Adresse des Committers verbunden error.no_gpg_keys_found=Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden error.not_signed_commit=Kein signierter Commit error.failed_retrieval_gpg_keys=Fehler beim Abrufen eines Keys des Commiter-Kontos +error.probable_bad_signature=WARNHINWEIS! Obwohl es einen Schlüssel mit dieser ID in der Datenbank gibt, wird dieser Commit nicht verifiziert! Dieser Commit ist nicht vertrauenswürdig. +error.probable_bad_default_signature=WARNHINWEIS! Obwohl der Standardschlüssel diese ID hat, wird der Commit nicht verifiziert! Dieser Commit ist NICHT vertrauenswürdig. [units] error.no_unit_allowed_repo=Du hast keine Berechtigung, um auf irgendeinen Bereich dieses Repositories zuzugreifen. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 978a1dd5b1..8e83213fd7 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1,5 +1,3 @@ -app_desc = A painless, self-hosted Git service - home = Home dashboard = Dashboard explore = Explore @@ -75,6 +73,17 @@ write = Write preview = Preview loading = Loading… +[startpage] +app_desc = A painless, self-hosted Git service +install = Easy to install +install_desc = Simply run the binary for your platform. Or ship Gitea with Docker or Vagrant, or get it packaged. +platform = Cross-platform +platform_desc = Gitea runs anywhere Go can compile for: Windows, macOS, Linux, ARM, etc. Choose the one you love! +lightweight = Lightweight +lightweight_desc = Gitea has low minimal requirements and can run on an inexpensive Raspberry Pi. Save your machine energy! +license = Open Source +license_desc = Go get code.gitea.io/gitea! Join us by contributing to make this project even better. Don't be shy to be a contributor! + [install] install = Installation title = Initial Configuration @@ -250,7 +259,7 @@ openid_signin_desc = Enter your OpenID URI. For example: https://anne.me, bob.op disable_forgot_password_mail = Account recovery is disabled. Please contact your site administrator. email_domain_blacklisted = You cannot register with your email address. authorize_application = Authorize Application -authroize_redirect_notice = You will be redirected to %s if you authorize this application. +authorize_redirect_notice = You will be redirected to %s if you authorize this application. authorize_application_created_by = This application was created by %s. authorize_application_description = If you grant the access, it will be able to access and write to all your account information, including private repos and organisations. authorize_title = Authorize "%s" to access your account? @@ -315,6 +324,7 @@ team_no_units_error = Allow access to at least one repository section. email_been_used = The email address is already used. openid_been_used = The OpenID address '%s' is already used. username_password_incorrect = Username or password is incorrect. +password_complexity = Password does not pass complexity requirements. enterred_invalid_repo_name = The repository name you entered is incorrect. enterred_invalid_owner_name = The new owner name is not valid. enterred_invalid_password = The password you entered is incorrect. @@ -633,6 +643,8 @@ migrate.lfs_mirror_unsupported = Mirroring LFS objects is not supported - use 'g migrate.migrate_items_options = When migrating from github, input a username and migration options will be displayed. migrated_from = Migrated from %[2]s migrated_from_fake = Migrated From %[1]s +migrate.migrating = Migrating from %s ... +migrate.migrating_failed = Migrating from %s failed. mirror_from = mirror of forked_from = forked from @@ -681,6 +693,8 @@ stored_lfs = Stored with Git LFS commit_graph = Commit Graph blame = Blame normal_view = Normal View +line = line +lines = lines editor.new_file = New File editor.upload_file = Upload File @@ -721,6 +735,8 @@ editor.file_editing_no_longer_exists = The file being edited, '%s', no longer ex editor.file_deleting_no_longer_exists = The file being deleted, '%s', no longer exists in this repository. editor.file_changed_while_editing = The file contents have changed since you started editing. Click here to see them or Commit Changes again to overwrite them. editor.file_already_exists = A file named '%s' already exists in this repository. +editor.commit_empty_file_header = Commit an empty file +editor.commit_empty_file_text = The file you're about commit is empty. Proceed? editor.no_changes_to_show = There are no changes to show. editor.fail_to_update_file = Failed to update/create file '%s' with error: %v editor.add_subdir = Add a directory… @@ -786,6 +802,7 @@ issues.delete_branch_at = `deleted branch %s %s` issues.open_tab = %d Open issues.close_tab = %d Closed issues.filter_label = Label +issues.filter_label_exclude = `Use alt + click/enter to exclude labels` issues.filter_label_no_select = All labels issues.filter_milestone = Milestone issues.filter_milestone_no_select = All milestones @@ -960,6 +977,7 @@ issues.review.review = Review issues.review.reviewers = Reviewers issues.review.show_outdated = Show outdated issues.review.hide_outdated = Hide outdated +issues.assignee.error = Not all assignees was added due to an unexpected error. pulls.desc = Enable pull requests and code reviews. pulls.new = New Pull Request @@ -1318,6 +1336,7 @@ settings.protect_this_branch = Enable Branch Protection settings.protect_this_branch_desc = Prevent deletion and disable any Git pushing to the branch. settings.protect_whitelist_committers = Enable Push Whitelist settings.protect_whitelist_committers_desc = Allow whitelisted users or teams to push to this branch (but not force push). +settings.protect_whitelist_deploy_keys = Whitelist deploy keys with write access to push settings.protect_whitelist_users = Whitelisted users for pushing: settings.protect_whitelist_search_users = Search users… settings.protect_whitelist_teams = Whitelisted teams for pushing: @@ -1365,7 +1384,10 @@ diff.parent = parent diff.commit = commit diff.git-notes = Notes diff.data_not_available = Diff Content Not Available -diff.show_diff_stats = Show Diff Stats +diff.options_button = Diff Options +diff.show_diff_stats = Show Stats +diff.download_patch = Download Patch File +diff.download_diff = Download Diff File diff.show_split_view = Split View diff.show_unified_view = Unified View diff.whitespace_button = Whitespace @@ -1448,6 +1470,8 @@ branch.restore_failed = Failed to restore branch '%s'. branch.protected_deletion_failed = Branch '%s' is protected. It cannot be deleted. branch.restore = Restore Branch '%s' branch.download = Download Branch '%s' +branch.included_desc = This branch is part of the default branch +branch.included = Included topic.manage_topics = Manage Topics topic.done = Done @@ -1969,12 +1993,15 @@ mark_as_unread = Mark as unread mark_all_as_read = Mark all as read [gpg] +default_key=Signed with default key error.extract_sign = Failed to extract signature error.generate_hash = Failed to generate hash of commit error.no_committer_account = No account linked to committer's email address error.no_gpg_keys_found = "No known key found for this signature in database" error.not_signed_commit = "Not a signed commit" error.failed_retrieval_gpg_keys = "Failed to retrieve any key attached to the committer's account" +error.probable_bad_signature = "WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS." +error.probable_bad_default_signature = "WARNING! Although the default key has this ID it does not verify this commit! This commit is SUSPICIOUS." [units] error.no_unit_allowed_repo = You are not allowed to access any section of this repository. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 675de019b0..1488cf9ba2 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1,5 +1,3 @@ -app_desc=Un servicio de Git auto alojado y sin complicaciones - home=Inicio dashboard=Panel de control explore=Explorar @@ -66,7 +64,7 @@ collaborative=Colaborativos forks=Forks activities=Actividad -pull_requests=Peticiones pull +pull_requests=Pull Requests issues=Incidencias cancel=Cancelar @@ -75,6 +73,17 @@ write=Escribir preview=Vista previa loading=Cargando… +[startpage] +app_desc=Un servicio de Git autoalojado y sin complicaciones +install=Fácil de instalar +install_desc=Simplemente arranca el binario para tu plataforma. O usa Gitea con Docker o Vagrant, o utilice el paquete. +platform=Multiplataforma +platform_desc=Gitea funciona en cualquier platforma Go puede compilarlo en: Windows, macOS, Linux, ARM, etc. ¡Elige tu favorita! +lightweight=Ligero +lightweight_desc=Gitea tiene pocos requisitos y puede funcionar en una Raspberry Pi barata. ¡Ahorra energía! +license=Código abierto +license_desc=¡Está todo en GitHub! Uniros contribuyendo a hacer este proyecto todavía mejor. ¡No seas tímido y colabora! + [install] install=Instalación title=Configuración inicial @@ -250,7 +259,6 @@ openid_signin_desc=Introduzca su URI OpenID. Por ejemplo: https://anne.me, bob.o disable_forgot_password_mail=La recuperación de cuentas está desactivada. Por favor, contacte con el administrador del sitio. email_domain_blacklisted=No puede registrarse con su correo electrónico. authorize_application=Autorizar aplicación -authroize_redirect_notice=Será redirigido a %s si autoriza esta aplicación. authorize_application_created_by=Esta aplicación fue creada por %s. authorize_application_description=Si concede el acceso, podrá acceder y escribir a toda la información de su cuenta, incluyendo repositorios privado y organizaciones. authorize_title=¿Autorizar a "%s" a acceder a su cuenta? @@ -299,6 +307,7 @@ max_size_error=` debe contener como máximo %s caracteres.` email_error=` no es una dirección de correo válida.` url_error=` no es una URL válida.` include_error=` debe contener la subcadena '%s'.` +glob_pattern_error=` el patrón globo no es válido: %s.` unknown_error=Error desconocido: captcha_incorrect=El código CAPTCHA no es correcto. password_not_match=Las contraseñas no coinciden. @@ -313,10 +322,12 @@ team_no_units_error=Permitir el acceso a por lo menos una sección del repositor email_been_used=La dirección de correo electrónico ya está usada. openid_been_used=La dirección OpenID '%s' ya está usada. username_password_incorrect=El nombre de usuario o la contraseña son incorrectos. +password_complexity=La contraseña no pasa por los requisitos de complejidad. enterred_invalid_repo_name=El nombre de repositorio que ha entrado es incorrecto. enterred_invalid_owner_name=El nuevo nombre de usuario no es válido. enterred_invalid_password=La contraseña que ha introducido es incorrecta. user_not_exist=Este usuario no existe. +team_not_exist=Este equipo no existe. last_org_owner=No puedes eliminar al último usuario del equipo de 'propietarios'. Debe haber al menos un propietario en ningún equipo dado. cannot_add_org_to_team=Una organización no puede ser añadida como miembro de un equipo. @@ -556,12 +567,17 @@ confirm_delete_account=Confirmar Eliminación delete_account_title=Eliminar cuenta de usuario delete_account_desc=¿Está seguro que desea eliminar permanentemente esta cuenta de usuario? +email_notifications.enable=Habilitar notificaciones por correo electrónico +email_notifications.onmention=Enviar correo sólo al ser mencionado +email_notifications.disable=Deshabilitar las notificaciones por correo electrónico +email_notifications.submit=Establecer preferencias de correo electrónico [repo] owner=Propietario repo_name=Nombre del repositorio repo_name_helper=Un buen nombre de repositorio está compuesto por palabras clave cortas, memorables y únicas. visibility=Visibilidad +visibility_description=Sólo el propietario o los miembros de la organización -si tienen derechos- podrán verlo. visibility_helper=Hacer repositorio privado visibility_helper_forced=El administrador de su sitio obliga a nuevos repositorios a ser privados. visibility_fork_helper=(Cambiar esto afectará a todos los forks) @@ -572,6 +588,8 @@ fork_visibility_helper=La visibilidad de un repositorio del cual se ha hecho for repo_desc=Descripción repo_lang=Idioma repo_gitignore_helper=Seleccionar plantillas de .gitignore. +issue_labels=Etiquetas de incidencia +issue_labels_helper=Seleccione un conjunto de etiquetas de incidencia. license=Licencia license_helper=Seleccione un archivo de licencia. readme=LÉAME @@ -584,6 +602,7 @@ mirror_prune_desc=Eliminar referencias de seguimiento de remotes obsoletas mirror_interval=Intervalo de réplica (Las unidades de tiempo válidas son 'h', 'm', 's'). Pone 0 para deshabilitar la sincronización automática. mirror_interval_invalid=El intervalo de réplica no es válido. mirror_address=Clonar desde URL +mirror_address_desc=Agregue las credenciales que sean necesarias en la sección de Autorización de Clonado. mirror_address_url_invalid=La url proporcionada no es válida. Debe escapar correctamente de todos los componentes de la url. mirror_address_protocol_invalid=La url proporcionada no es válida. Sólo las ubicaciones http(s):// o git:// pueden ser replicadas desde. mirror_last_synced=Sincronizado por última vez @@ -622,6 +641,8 @@ migrate.lfs_mirror_unsupported=La replicación de objetos LFS no está soportada migrate.migrate_items_options=Cuando migra desde github, se mostrarán un nombre de usuario y se mostrarán opciones de migración. migrated_from=Migrado desde %[2]s migrated_from_fake=Migrado desde %[1]s +migrate.migrating=Migrando desde %s... +migrate.migrating_failed=La migración desde %s ha fallado. mirror_from=réplica de forked_from=forkeado de @@ -653,7 +674,7 @@ filter_branch_and_tag=Filtrar por rama o etiqueta branches=Ramas tags=Etiquetas issues=Incidencias -pulls=Peticiones pull +pulls=Pull Requests labels=Etiquetas milestones=Hitos commits=Commits @@ -670,6 +691,8 @@ stored_lfs=Almacenados con Git LFS commit_graph=Gráfico de commits blame=Blame normal_view=Vista normal +line=línea +lines=líneas editor.new_file=Nuevo Archivo editor.upload_file=Subir archivo @@ -695,6 +718,7 @@ editor.delete=Eliminar '%s' editor.commit_message_desc=Añadir una descripción extendida opcional… editor.commit_directly_to_this_branch=Hacer commit directamente en la rama %s. editor.create_new_branch=Crear una nueva rama para este commit y hacer un pull request. +editor.create_new_branch_np=Crear una nueva rama para este commit. editor.propose_file_change=Proponer cambio de archivo editor.new_branch_name_desc=Nombre de la rama nueva… editor.cancel=Cancelar @@ -709,6 +733,8 @@ editor.file_editing_no_longer_exists=El archivo que está editando, '%s', ya no editor.file_deleting_no_longer_exists=El archivo que se está eliminando, '%s', ya no existe en este repositorio. editor.file_changed_while_editing=Desde que comenzó a editar, el contenido del archivo ha sido cambiado. Clic aquí para ver qué ha cambiado o presione confirmar de nuevo para sobrescribir los cambios. editor.file_already_exists=Ya existe un archivo con nombre '%s' en este repositorio. +editor.commit_empty_file_header=Commit un archivo vacío +editor.commit_empty_file_text=El archivo que estás tratando de commit está vacío. ¿Proceder? editor.no_changes_to_show=No existen cambios para mostrar. editor.fail_to_update_file=Error al actualizar/crear el archivo '%s', error: %v editor.add_subdir=Añadir un directorio… @@ -769,6 +795,7 @@ issues.self_assign_at=`auto asignado este %s` issues.add_assignee_at='fue asignado por %s %s' issues.remove_assignee_at=`fue desasignado por %s %s` issues.remove_self_assignment=`eliminado su asignación %s` +issues.change_title_at=`cambió el título de %s a %s %s` issues.delete_branch_at=`rama eliminada %s %s` issues.open_tab=%d abiertas issues.close_tab=%d cerradas @@ -825,6 +852,10 @@ issues.create_comment=Comentar issues.closed_at=`cerró %[2]s` issues.reopened_at=`reabrió %[2]s` issues.commit_ref_at=`mencionada esta incidencia en un commit %[2]s` +issues.ref_issue_at=`referenciada esta incidencia %[1]s` +issues.ref_pull_at=`refereciada este Pull Request %[1]s` +issues.ref_issue_ext_at=`referenciada esta incidencia desde %[1]s %[2]s` +issues.ref_pull_ext_at=`refereciada este Pull Request desde %[1]s %[2]s` issues.poster=Autor issues.collaborator=Colaborador issues.owner=Propietario @@ -944,6 +975,7 @@ issues.review.reviewers=Revisores issues.review.show_outdated=Mostrar obsoletos issues.review.hide_outdated=Ocultar obsoletos +pulls.desc=Activar Pull Requests y revisiones de código. pulls.new=Nuevo Pull Request pulls.compare_changes=Nuevo pull request pulls.compare_changes_desc=Seleccione la rama en la que se fusiona y la rama a recuperar. @@ -959,15 +991,18 @@ pulls.merged_title_desc=fusionados %[1]d commits de %[2]s en pulls.tab_conversation=Conversación pulls.tab_commits=Commits pulls.tab_files=Archivos modificados -pulls.reopen_to_merge=Vuelva a abrir la solicitud de "pull" para realizar una fusión. +pulls.reopen_to_merge=Vuelva a abrir este Pull Request para realizar una fusión. pulls.cant_reopen_deleted_branch=Este pull request no se puede reabrir porque la rama fue eliminada. pulls.merged=Fusionado +pulls.merged_as=El Pull Request se ha fusionado como %[2]s. pulls.has_merged=El pull request ha sido fusionado. pulls.title_wip_desc=`Comience el título con %s para prevenir que el pull request se fusione accidentalmente.` pulls.cannot_merge_work_in_progress=Este pull request está marcado como un trabajo en progreso. Elimine el prefijo %s del título cuando esté listo pulls.data_broken=Este pull request está rota debido a que falta información del fork. pulls.files_conflicted=Este pull request tiene cambios en conflicto con la rama de destino. pulls.is_checking=La comprobación de conflicto de fusión está en progreso. Inténtalo de nuevo en unos momentos. +pulls.required_status_check_failed=Algunos controles requeridos no han tenido éxito. +pulls.required_status_check_administrator=Como administrador, aún puede fusionar este Pull Request. pulls.blocked_by_approvals=Este pull request aún no tiene suficientes aprobaciones. %d de %d autorizaciones concedidas. pulls.can_auto_merge_desc=Este Pull Request puede ser fusionado automáticamente. pulls.cannot_auto_merge_desc=Este pull request no se puede combinar automáticamente debido a conflictos. @@ -975,6 +1010,7 @@ pulls.cannot_auto_merge_helper=Combinar manualmente para resolver los conflictos pulls.no_merge_desc=Este pull request no se puede combinar porque todas las opciones de combinación del repositorio están deshabilitadas. pulls.no_merge_helper=Habilite las opciones de combinación en la configuración del repositorio o fusione el pull request manualmente. pulls.no_merge_wip=Este pull request no se puede combinar porque está marcada como un trabajo en progreso. +pulls.no_merge_status_check=No se puede fusionar este pull request porque no todas las comprobaciones de estado requeridas resultaron exitosas. pulls.merge_pull_request=Fusionar Pull Request pulls.rebase_merge_pull_request=Hacer Rebase y Fusionar pulls.rebase_merge_commit_pull_request=Hacer Rebase y Fusionar (--no-ff) @@ -1116,6 +1152,7 @@ settings.collaboration=Colaboradores settings.collaboration.admin=Administrador settings.collaboration.write=Escritura settings.collaboration.read=Lectura +settings.collaboration.owner=Propietario settings.collaboration.undefined=Indefinido settings.hooks=Webhooks settings.githooks=Git Hooks @@ -1123,6 +1160,10 @@ settings.basic_settings=Configuración Básica settings.mirror_settings=Configuración de réplica settings.sync_mirror=Sincronizar ahora settings.mirror_sync_in_progress=La sincronización del repositorio replicado está en curso. Vuelva a intentarlo más tarde. +settings.email_notifications.enable=Habilitar las notificaciones por correo electrónico +settings.email_notifications.onmention=Enviar correo sólo al mencionar +settings.email_notifications.disable=Deshabilitar las notificaciones por correo electrónico +settings.email_notifications.submit=Establecer Preferencia de correo electrónico settings.site=Sitio web settings.update_settings=Actualizar configuración settings.advanced_settings=Ajustes avanzados @@ -1193,6 +1234,11 @@ settings.collaborator_deletion_desc=Eliminar un colaborador revocará su acceso settings.remove_collaborator_success=El colaborador ha sido eliminado. settings.search_user_placeholder=Buscar usuario… settings.org_not_allowed_to_be_collaborator=Las organizaciones no pueden ser añadidas como colaboradoras. +settings.change_team_access_not_allowed=Cambiar el acceso del equipo al repositorio se ha restringido al propietario de la organización +settings.team_not_in_organization=El equipo no pertenece a la misma organización que el repositorio +settings.add_team_duplicate=El equipo ya tiene acceso al repositorio +settings.add_team_success=Ahora el equipo ya tiene acceso al repositorio. +settings.remove_team_success=Se ha eliminado el acceso del equipo al repositorio. settings.add_webhook=Añadir Webhook settings.add_webhook.invalid_channel_name=El nombre del canal Webhook no puede estar vacío y no puede contener sólo un # carácter. settings.hooks_desc=Los webhooks automáticamente hacen peticiones HTTP POST a un servidor cuando ciertos eventos de Gitea se activan. Lee más en la guía de webhooks. @@ -1242,6 +1288,8 @@ settings.event_pull_request=Pull Request settings.event_pull_request_desc=Pull Request abierta, cerrada, reabierta, editada, aprobada, rechazada, comentario de revisión, asignada, no asignada, etiqueta actualizada, etiqueta borrada o sincronizada. settings.event_push=Push settings.event_push_desc=Git push a un repositorio. +settings.branch_filter=Filtro de rama +settings.branch_filter_desc=Lista blanca de rama para eventos de push, creación de rama y eliminación de rama, especificados como patrón globo. Si está vacío o *, se reportan eventos para todas las ramas. Ver github.com/gobwas/glob documentación para la sintaxis. Ejemplos: master, {master,release*}. settings.event_repository=Repositorio settings.event_repository_desc=Repositorio creado o eliminado. settings.active=Activo @@ -1260,20 +1308,20 @@ settings.add_discord_hook_desc=Integrar Discord en su repositor settings.add_dingtalk_hook_desc=Integrar Dingtalk en su repositorio. settings.add_telegram_hook_desc=Integrar Telegrama en tu repositorio. settings.add_msteams_hook_desc=Integrar Microsoft Teams en tu repositorio. -settings.deploy_keys=Claves de Despliegue -settings.add_deploy_key=Añadir Clave de Despliegue -settings.deploy_key_desc=Las claves de despliegue tienen acceso de sólo lectura al repositorio. +settings.deploy_keys=Claves de Implementación +settings.add_deploy_key=Añadir Clave de Implementación +settings.deploy_key_desc=Las claves de implementación tienen acceso de sólo lectura al repositorio. settings.is_writable=Habilitar acceso de escritura -settings.is_writable_info=Permitir que esta clave de despliegue pueda hacer push a este repositorio. -settings.no_deploy_keys=Aún no existen claves de despliegue. +settings.is_writable_info=Permitir que esta clave de implementación pueda hacer push a este repositorio. +settings.no_deploy_keys=Aún no existen claves de implementación. settings.title=Título settings.deploy_key_content=Contenido -settings.key_been_used=Una clave de despliegue con contenido idéntico ya se encuentra en uso. -settings.key_name_used=Ya existe una clave de despliegue con el mismo nombre. -settings.add_key_success=La clave de despliegue '%s' ha sido añadida. -settings.deploy_key_deletion=Eliminar clave de despliegue -settings.deploy_key_deletion_desc=Eliminar una clave de despliegue revocará el acceso de la misma a este repositorio. ¿Continuar? -settings.deploy_key_deletion_success=La clave de despliegue ha sido eliminada. +settings.key_been_used=Una clave de implementación con contenido idéntico ya se encuentra en uso. +settings.key_name_used=Ya existe una clave de implementación con el mismo nombre. +settings.add_key_success=La clave de implementación '%s' ha sido añadida. +settings.deploy_key_deletion=Eliminar clave de implementación +settings.deploy_key_deletion_desc=Eliminar una clave de implementación revocará el acceso de la misma a este repositorio. ¿Continuar? +settings.deploy_key_deletion_success=La clave de implementación ha sido eliminada. settings.branches=Ramas settings.protected_branch=Protección de rama settings.protected_branch_can_push=¿Permitir hacer push? @@ -1284,6 +1332,7 @@ settings.protect_this_branch=Activar protección de rama settings.protect_this_branch_desc=Prevenir eliminar y desactivar hacer git push en esta rama. settings.protect_whitelist_committers=Activar lista blanca para hacer push settings.protect_whitelist_committers_desc=Permitir hacer push en esta rama a los usuarios o equipos en la lista blanca (pero no hacer push forzado). +settings.protect_whitelist_deploy_keys=Poner en lista blanca las claves de implementación con permisos de hacer push settings.protect_whitelist_users=Usuarios en la lista blanca para hacer push: settings.protect_whitelist_search_users=Buscar usuarios… settings.protect_whitelist_teams=Equipos en la lista blanca para hacer push: @@ -1292,6 +1341,9 @@ settings.protect_merge_whitelist_committers=Activar lista blanca para fusionar settings.protect_merge_whitelist_committers_desc=Permitir a los usuarios o equipos de la lista a fusionar peticiones pull dentro de esta rama. settings.protect_merge_whitelist_users=Usuarios en la lista blanca para fusionar: settings.protect_merge_whitelist_teams=Equipos en la lista blanca para fusionar: +settings.protect_check_status_contexts=Habilitar comprobación de estado +settings.protect_check_status_contexts_desc=Requiere comprobaciones de estado para pasar antes de fusionar. Elija qué los controles de estado deben pasar antes de que las ramas puedan ser fusionadas en una rama que coincida con esta regla. Cuando está habilitada, los commits deben ser primero empujados a otra rama y luego fusionados, o empujados directamente a una rama que coincida con esta regla después de que hayan pasado los comprobaciones de estado. Si no se selecciona ningún contexto, la última confirmación debe tener éxito independientemente del contexto. +settings.protect_check_status_contexts_list=Comprobaciones de estado para este repositorio encontradas durante la semana pasada settings.protect_required_approvals=Aprobaciones requeridas: settings.protect_required_approvals_desc=Permitir solo fusionar un pull request con suficientes revisiones positivas de usuarios o equipos en la lista blanca. settings.protect_approvals_whitelist_users=Lista blanca de usuarios revisores: @@ -1328,7 +1380,10 @@ diff.parent=padre diff.commit=commit diff.git-notes=Notas diff.data_not_available=El contenido del Diff no está disponible -diff.show_diff_stats=Mostrar estadísticas de diff +diff.options_button=Opciones de diferencias +diff.show_diff_stats=Mostrar estadísticas +diff.download_patch=Descargar archivo de parche +diff.download_diff=Descargar archivo de diferencias diff.show_split_view=Dividir vista diff.show_unified_view=Unificar vista diff.whitespace_button=Espacio blanco @@ -1339,6 +1394,11 @@ diff.whitespace_ignore_at_eol=Ignorar cambios en espacios en blanco al final de diff.stats_desc=Se han modificado %d ficheros con %d adiciones y %d borrados diff.bin=BIN diff.view_file=Ver fichero +diff.file_before=Antes +diff.file_after=Después +diff.file_image_width=Anchura +diff.file_image_height=Altura +diff.file_byte_size=Tamaño diff.file_suppressed=La diferencia del archivo ha sido suprimido porque es demasiado grande diff.too_many_files=Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio diff.comment.placeholder=Deja un comentario @@ -1406,6 +1466,8 @@ branch.restore_failed=Fallo al restaurar la rama %s. branch.protected_deletion_failed=La rama '%s' está protegida. No se puede eliminar. branch.restore=Restaurar rama '%s' branch.download=Descargar rama '%s' +branch.included_desc=Esta rama forma parte de la predeterminada +branch.included=Incluida topic.manage_topics=Administrar temas topic.done=Hecho @@ -1441,6 +1503,8 @@ settings.options=Organización settings.full_name=Nombre completo settings.website=Página web settings.location=Localización +settings.permission=Permisos +settings.repoadminchangeteam=El administrador del repositorio puede añadir y eliminar el acceso a equipos settings.visibility=Visibilidad settings.visibility.public=Público settings.visibility.limited=Limitado (Visible sólo para los usuarios registrados) @@ -1672,7 +1736,7 @@ auths.oauth2_use_custom_url=Utilizar URLs personalizadas en vez de direcciones U auths.oauth2_tokenURL=URL del token auths.oauth2_authURL=URL de Autorización auths.oauth2_profileURL=URL del perfil -auths.oauth2_emailURL=URL de email +auths.oauth2_emailURL=URL de correo auths.enable_auto_register=Hablilitar Auto-Registro auths.tips=Consejos auths.tips.oauth2.general=Autenticación OAuth2 @@ -1687,6 +1751,7 @@ auths.tip.google_plus=Obtener credenciales de cliente OAuth2 desde la consola AP auths.tip.openid_connect=Use el OpenID Connect Discovery URL (/.well-known/openid-configuration) para especificar los puntos finales auths.tip.twitter=Ir a https://dev.twitter.com/apps, crear una aplicación y asegurarse de que la opción "Permitir que esta aplicación sea usada para iniciar sesión con Twitter" está activada auths.tip.discord=Registrar una nueva aplicación en https://discordapp.com/developers/applications/me +auths.tip.gitea=Registra una nueva aplicación OAuth2. La guía puede ser encontrada en https://docs.gitea.io/es-us/oauth2-provider/ auths.edit=Editar origen de autenticación auths.activated=Este origen de autenticación ha sido activado auths.new_success=Se agregó la autenticación '%s'. @@ -1919,12 +1984,15 @@ mark_as_unread=Marcar como no leído mark_all_as_read=Marcar todo como leído [gpg] +default_key=Firmado con clave predeterminada error.extract_sign=Error al extraer la firma error.generate_hash=Error al generar hash of commit error.no_committer_account=Ninguna cuenta vinculada a la dirección de correo electrónico del committer error.no_gpg_keys_found=No se encontró ninguna clave conocida en la base de datos para esta firma error.not_signed_commit=No es un commit firmado error.failed_retrieval_gpg_keys=No se pudo recuperar cualquier clave adjunta a la cuenta del committer +error.probable_bad_signature=¡ADVERTENCIA! ¡Hay una clave con este ID en la base de datos, pero esa clave no verifica este commit! Este commit es SOSPECHOSO. +error.probable_bad_default_signature=¡ADVERTENCIA! ¡La clave por defecto tiene este ID pero esa clave no verifica este commit! Este commit es SOSPECHOSO. [units] error.no_unit_allowed_repo=No tiene permisos para acceder a ninguna sección de este repositorio. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 0ff49d19d3..a0551ed71d 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -1,5 +1,3 @@ -app_desc=یک سرویس گیت بی‌درد سر و راحت - home=خانه dashboard=میز کار explore=گشت‌و‌گذار @@ -75,6 +73,17 @@ write=نوشتن preview=پیش نمایش loading=بارگذاری… +[startpage] +app_desc=یک سرویس گیت بی‌درد سر و راحت +install=راه‌اندازی ساده +install_desc=به سادگی دودویی را اجرا بر روی سکوی خود کنید. یا shop Gitea زا یل داکر یا Vagrant ,یا این را دریافت کنید. بسته بندی شده +platform=مستقل از سکو +platform_desc=گیت همه جا اجرا می‌شود بریم! می‌توانید Windows, macOS, Linux, ARM و ... هر کدام را دوست داشتید انتخاب کنید! +lightweight=ابزارک سبک +lightweight_desc=گیتی با حداقل منابع میتوانید برای روی دستگاه Raspberry Pi اجرا شود و مصرف انرژی شما را کاهش دهد! +license=متن باز +license_desc=برو به code.gitea.io/gitea! به ملحق شوید با مشارکت کردن برای این که این پروژه بهتر شود. برای مشارکت کردن خجالت نکشید! + [install] install=نصب و راه اندازی title=تنظیمات اولیه @@ -250,7 +259,7 @@ openid_signin_desc=نوع حساب کاربری خود را وارد کنید. disable_forgot_password_mail=بازیابی حساب غیر فعال شده است. لطفا با مدیر سایت تماس بگیرید. email_domain_blacklisted=شما نمیتوانید با ایمیل خود ثبت نام کنید. authorize_application=برنامه احراز هویت -authroize_redirect_notice=اگر شما این برنامه را تایید کنید، به %s منتقل خواهید شد. +authorize_redirect_notice=اگر شما این برنامه را تایید کنید، به %s منتقل خواهید شد. authorize_application_created_by=این برنامه توسط %s ساخته شده است. authorize_application_description=اگر شما دسترسی داشته باشید. میتوانید تمامی فیلد های حساب کاربری خود را تغییر دهید. از جمله مخازن و سازمان های خصوصی. authorize_title=تاییدیه "%s" برای دسترسی به اکانت شما؟ @@ -314,10 +323,12 @@ team_no_units_error=اجازه دسترسی به حداقل یک بخش مخزن email_been_used=این ایمیل قبلا ثبت شده. openid_been_used=آدرس OpenID %s قبلا ثبت شده است. username_password_incorrect=نام کاربری یا گذرواژه صحیح نیست. +password_complexity=پسورد ضرورت به پیچیدگی نداشته باشد. enterred_invalid_repo_name=نام مخزنی که وارد کرده اید صحیح نمی باشد. enterred_invalid_owner_name=نام مالک جدید معتبر نیست. enterred_invalid_password=گذرواژه وارد شده صحیح نیست. user_not_exist=کاربر وجود ندارد. +team_not_exist=تیم وجود ندارد. last_org_owner=قادر به حذف کاربر آخر از تیم "صاحبان" نیست. باید حداقل یک مالک در هر تیم باشد. cannot_add_org_to_team=یک سازمان را نمی توان به عنوان عضو تیم اضافه کرد. @@ -634,6 +645,8 @@ migrate.lfs_mirror_unsupported=قرینه سازی LFS اشیا پشتیبانی migrate.migrate_items_options=زمانی که از github مهاجرت می‌کنید. ورودی نام‌کاربری و گزینه‌های مهاجرت نمایش داده می‌شوند. migrated_from=مهاجرت از %[2]s migrated_from_fake=مهاجرت از %[1]s +migrate.migrating=مهاجرت از %s ... +migrate.migrating_failed=مهاجرت از %s ناموفق بود. mirror_from=قرینه از forked_from=انشعاب شده از @@ -682,6 +695,8 @@ stored_lfs=ذخیره شده با GIT LFS commit_graph=نمودار کامیت blame=سرزنش normal_view=نمایش عادی +line=خط +lines=خطوط editor.new_file=پرونده جدید editor.upload_file=بارگذاری پرونده @@ -707,6 +722,7 @@ editor.delete=حذف '%s' editor.commit_message_desc=توضیحی تخصصی به دلخواه اضافه نمایید… editor.commit_directly_to_this_branch=ثبت کامیت به صورت مستقیم در انشعاب %s. editor.create_new_branch=یک شاخه جدید برای این commit ایجاد کنید و تقاضای واکشی را شروع کنید. +editor.create_new_branch_np=یک شاخه جدید برای کامیت بسازید. editor.propose_file_change=پیشنهاد تغییر پرونده editor.new_branch_name_desc=نام شاخه ی جدید… editor.cancel=انصراف @@ -721,6 +737,8 @@ editor.file_editing_no_longer_exists=فایل آماده ویرایش شده ا editor.file_deleting_no_longer_exists=فایل آماده حذف می‌شود '%s'، مدتی بعد در مخزن از دسترس خارج می‎شود. editor.file_changed_while_editing=محتوای پرونده تغییر میکند از زمانی که شما شروع به ویرایش می‌کنید.اینجا کلیک کنید تا ببنید آن را یا یا کامیت تغییرات را دوباره اعمال کنید تا روی آن بازنویسی شود. editor.file_already_exists=فایلی با نام %s از قبل در مخزن موجود است. +editor.commit_empty_file_header=کامیت کردن یک پرونده خالی +editor.commit_empty_file_text=فایل کامیت شده شما تقریبا خالیست. پردازش شود؟ editor.no_changes_to_show=تغییری برای نمایش وجود ندارد. editor.fail_to_update_file=خطا در ساخت/به‌روزرسانی پرونده %s. خطای رخ داده: %v editor.add_subdir=افزودن پوشه… @@ -786,6 +804,7 @@ issues.delete_branch_at=`حذف شاخه %s %s` issues.open_tab=%d باز issues.close_tab=%d بسته issues.filter_label=برچسب +issues.filter_label_exclude=`ازalt + click/enter برای رد کردن برچسب‌ها استفاده کنید ` issues.filter_label_no_select=تمامی برچسب‎ها issues.filter_milestone=نقطه عطف issues.filter_milestone_no_select=تمام نقاط عطف @@ -988,6 +1007,7 @@ pulls.data_broken=این تقاضای واکشی به دلیل از دست رف pulls.files_conflicted=این تقاضای واکشی دارای تغییراتی است که با شاخه هدف تداخل دارد. pulls.is_checking=در حال پردازش تداخل در ادغام می‌باشد. لطفاً لحظاتی بعد امتحان کنید. pulls.required_status_check_failed=برخی بررسی های ضروری موفقیت آمیز نبود. +pulls.required_status_check_administrator=مثل یک مدیر، ممکن است شما این تقاضای واکشی را مسکوت بگذارید. pulls.blocked_by_approvals=این تقاضای واکشی هنوز به اندازه کافی مورد مورد تایید نیست. %d از %d مورد آن قابل تایید می‌باشد. pulls.can_auto_merge_desc=این تقاضا واکشی می تواند به صورت خودکار ادغام شود. pulls.cannot_auto_merge_desc=این تقاضای واکشی به علت تداخل نمی تواند به صورت خودکار ادغام شود. @@ -1137,6 +1157,7 @@ settings.collaboration=همكار settings.collaboration.admin=مدیر settings.collaboration.write=نوشتن settings.collaboration.read=خواندن +settings.collaboration.owner=مالک settings.collaboration.undefined=تعریف نشده settings.hooks=Webhooks settings.githooks=Git Hooks @@ -1218,6 +1239,11 @@ settings.collaborator_deletion_desc=حذف یک همکار از مخزن دست settings.remove_collaborator_success=همكار حذف شد. settings.search_user_placeholder=جستجوی کاربر… settings.org_not_allowed_to_be_collaborator=سازمان ها را نمیتوان به عنوان همکار افزود. +settings.change_team_access_not_allowed=تغییر دسترسی های تیم برای این مخزن توسط مالک ارگان محدود شده است +settings.team_not_in_organization=تیم همانند ارگان برای این مخزن نیست +settings.add_team_duplicate=تیم پیش از این مخزن داشته +settings.add_team_success=تیم هم‌اکنون به مخزن دسترسی دارد. +settings.remove_team_success=دسترسی تیم به مخزن حذف شد. settings.add_webhook=اضافه‌کردن Webhook settings.add_webhook.invalid_channel_name=کانال هوک تحت وب نمی‌تواند خالی باشد و نمی‌توانید تنها حاوی این حرف # باشد. settings.hooks_desc=هوک تحت وب به صورت خودکار درخواست POST HTTP را به سمت سرور روانه می‌کند زمانی که ماشه رخداد Gitea کشیده شود. برای اطلاعات بیشتر به راهنمای هوک تحت وب مراجعه کنید. @@ -1311,6 +1337,7 @@ settings.protect_this_branch=فعال کردن حفاظت از شاخه settings.protect_this_branch_desc=جلوگیری از هر گونه حذف یا غیرفعال کردن هر درج شدنی در این شاخه. settings.protect_whitelist_committers=فعال کردن لیست سفید درج settings.protect_whitelist_committers_desc=اجازه به کاربران یا تیم‌های موجود لیست سفید برای درج در این شاخه (اما نه درج اجباری). +settings.protect_whitelist_deploy_keys=لیست سفید کلید های انتشار و دسترسی نوشتن برای درج settings.protect_whitelist_users=کاربران لیست سفید برای درج در مخزن: settings.protect_whitelist_search_users=جستجوی کاربر… settings.protect_whitelist_teams=تیم‌های لیست سفید برای درج در مخزن: @@ -1320,6 +1347,7 @@ settings.protect_merge_whitelist_committers_desc=اجازه به کاربران settings.protect_merge_whitelist_users=کاربران لیست سفید برای ادغام: settings.protect_merge_whitelist_teams=تیم‌های لیست سفید برای ادغام: settings.protect_check_status_contexts=فعال کردن حالات بررسی +settings.protect_check_status_contexts_desc=حالات ضروری را میتوانید برای کنترل قبل از ادغام فعال کنید تا با قوانین شما اگر سازگار بود شاخه بتواند ادغام شود. زمانی که فعال شود، کامیت باید ابتدا در شاخه دیگری درج شود. settings.protect_check_status_contexts_list=آخرین بررسی حالات این مخزن در هفته گذشته اتفاق افتاده است settings.protect_required_approvals=نیازمند تاییدیه: settings.protect_required_approvals_desc=فقط اجازه تقاضای ادغام واکشی برای بازبینی‌کننده‌گانی که دارای امتیاز مثبت کافی که از لیست سفید کاربران یا تیم‌ها باشند. @@ -1357,7 +1385,10 @@ diff.parent=والد diff.commit=کامیت diff.git-notes=یادداشت‌ها diff.data_not_available=محتوای تفاوت ها در دسترس نیست -diff.show_diff_stats=نمایش آمار تفاوت ها +diff.options_button=تنظیمات (diff) تغییرات +diff.show_diff_stats=نمایش وضعیت +diff.download_patch=دانلود پرونده وصله +diff.download_diff=دانلود فایل تغییرات diff diff.show_split_view=مشاهده تقسیم شده diff.show_unified_view=نمای یکپارچه diff.whitespace_button=فضای خالی @@ -1440,6 +1471,8 @@ branch.restore_failed=ناموفق در بازگرانی شاخه '%s'. branch.protected_deletion_failed=شاخه «%s» محافظت شده است. نمی‌توانید آن‌را حذف کنید. branch.restore=بازگردانی شاخه '%s' branch.download=بارگیری یا دریافت شاخه '%s' +branch.included_desc=این شاخه بخشی از شاخه پیش فرض است +branch.included=مشمول شده topic.manage_topics=مدیریت موضوعات topic.done=انجام شد @@ -1475,6 +1508,8 @@ settings.options=سازمان settings.full_name=نام کامل settings.website=تارنما settings.location=موقعیت مکانی +settings.permission=مجوزها +settings.repoadminchangeteam=مدیر مخزن می‌تواند دسترسی برای تیم اضافه یا حذف کند settings.visibility=پدیداری settings.visibility.public=عمومی settings.visibility.limited=محدود شده (پدیدار برای کاربر وارد شده فقط) @@ -1955,12 +1990,15 @@ mark_as_unread=علامتگذاری بعنوان خوانده نشده mark_all_as_read=علامت همه به عنوان خوانده شده [gpg] +default_key=ثبت شده با کلید پیش فرض error.extract_sign=خطا در استخراج امضا error.generate_hash=خطا در ساختن هش کامیت error.no_committer_account=هیچ ایمیلی به حساب کاربری صاحب کامیت پیونده داده نشده است error.no_gpg_keys_found=هیچ کلید شناخته شده ای برای این امضا در پایگاه داده ها یافت نشد error.not_signed_commit=هیچ کامیتی تکلیف نشده است error.failed_retrieval_gpg_keys=بازیابی هر کلیدی که به حساب کاربری کامیت دهنده پیوست شده بود ناموفق بود +error.probable_bad_signature=هشدار! اگرچه اینجا یک کلید با ID در پایگاه داده است این کامیت تایید نشده است! این کامیت مشـــکــــوک است. +error.probable_bad_default_signature=هشدار! اگرچه اینجا یک کلید پیش فرض با ID است این اما کامیت تایید نشده است! این کامیت مشـــکــــوک است. [units] error.no_unit_allowed_repo=شما اجازه دسترسی به هیچ قسمت از این مخزن را ندارید. diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 12829c527b..75a80de528 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -1,5 +1,3 @@ -app_desc=Ongelmaton, itsehostattu Git-palvelu - home=Etusivu dashboard=Kojelauta explore=Tutki @@ -61,6 +59,8 @@ issues=Ongelmat cancel=Peruuta +[startpage] + [install] install=Asennus title=Alkuperäiset asetukset @@ -538,7 +538,6 @@ settings.edit_protected_branch=Muokkaa diff.browse_source=Selaa lähdekoodia diff.parent=vanhempi -diff.show_diff_stats=Näytä diff tilastot diff.show_split_view=Jaettu näkymä diff.show_unified_view=Yhdistetty näkymä diff.stats_desc=%d muutettua tiedostoa jossa %d lisäystä ja %d poistoa diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index e0e04f86a7..c9a8711533 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1,5 +1,3 @@ -app_desc=Un service Git auto-hébergé sans prise de tête - home=Accueil dashboard=Tableau de bord explore=Explorateur @@ -75,6 +73,15 @@ write=Écrire preview=Aperçu loading=Chargement… +[startpage] +app_desc=Un service Git auto-hébergé sans prise de tête +install=Facile à installer +install_desc=Il suffit de lancer l'exécutable correspondant à votre système. Ou d'utiliser Gitea avec Docker ou Vagrant ou en l'installant depuis un package. +platform=Multi-plateforme +platform_desc=Gitea tourne partout où Go peut être compilé : Windows, macOS, Linux, ARM, etc. Choisissez votre préféré ! +lightweight=Léger +license=Open Source + [install] install=Installation title=Configuration initiale @@ -250,7 +257,6 @@ openid_signin_desc=Veuillez entrer votre URI OpenID. Par exemple: https://anne.m disable_forgot_password_mail=La récupération de compte est désactivée. Veuillez contacter l'administrateur du site. email_domain_blacklisted=Vous ne pouvez pas vous enregistrer avec votre adresse e-mail. authorize_application=Autoriser l'application -authroize_redirect_notice=Vous serez redirigé vers %s si vous autorisez cette application. authorize_application_created_by=Cette application a été créée par %s. authorize_application_description=Si vous accordez l'accès, il sera en mesure d'accéder et d'écrire toutes les informations de votre compte, y compris les dépôts privés et les organisations. authorize_title=Autoriser "%s" à accéder à votre compte ? @@ -299,6 +305,7 @@ max_size_error=` %s caractères maximum ` email_error=` adresse e-mail invalide ` url_error=` URL invalide ` include_error=`doit contenir la sous-chaîne '%s'.` +glob_pattern_error=` le motif de développement est invalide : %s.` unknown_error=Erreur inconnue : captcha_incorrect=Le code CAPTCHA est incorrect. password_not_match=Les mots de passe ne correspondent pas. @@ -313,10 +320,12 @@ team_no_units_error=Autoriser l’accès à au moins une section du dépôt. email_been_used=Cette adresse e-mail est déjà utilisée. openid_been_used=Adresse OpenID '%s' déjà utilisée. username_password_incorrect=Identifiant ou mot de passe invalide. +password_complexity=Le mot de passe ne respecte pas les exigences de complexité. enterred_invalid_repo_name=Le nom de dépôt saisi est incorrect. enterred_invalid_owner_name=Le nom du nouveau propriétaire est invalide. enterred_invalid_password=Le mot de passe saisi est incorrect. user_not_exist=Cet utilisateur n'existe pas. +team_not_exist=L'équipe n'existe pas. last_org_owner=Vous ne pouvez pas supprimer le dernier utilisateur de l’équipe « propriétaires ». Il doit y avoir au moins un propriétaire dans chaque équipe. cannot_add_org_to_team=Une organisation ne peut être ajoutée comme membre d'une équipe. @@ -557,6 +566,7 @@ delete_account_title=Supprimer un compte delete_account_desc=Êtes-vous sûr de vouloir supprimer définitivement ce compte ? email_notifications.enable=Activer les notifications par e-mail +email_notifications.onmention=N'envoyer un e-mail que si vous êtes mentionné email_notifications.disable=Désactiver les notifications par email email_notifications.submit=Définir la préférence e-mail @@ -565,6 +575,7 @@ owner=Propriétaire repo_name=Nom du dépôt repo_name_helper=Idéalement, le nom d'un dépôt devrait être court, mémorisable et unique. visibility=Visibilité +visibility_description=Seul le propriétaire ou les membres de l'organisation s'ils ont des droits, seront en mesure de le voir. visibility_helper=Rendre le dépôt privé visibility_helper_forced=L'administrateur de votre serveur impose que les nouveaux dépôts soient privés. visibility_fork_helper=(Changer ceci affectera toutes les bifurcations.) @@ -587,6 +598,7 @@ mirror_prune_desc=Supprimer les références externes obsolètes mirror_interval=Intervalle de synchronisation ('h', 'm', et 's' sont des unités valides), 0 pour désactiver. mirror_interval_invalid=L'intervalle de synchronisation est invalide. mirror_address=Cloner depuis une URL +mirror_address_desc=Mettez les identifiants requis dans la section Autorisation de Clonage. mirror_address_url_invalid=L'url fournie est invalide. Vous devez échapper tous les composants de l'url correctement. mirror_address_protocol_invalid=L'url fournie est invalide. Seuls les protocoles http(s):// ou git:// peuvent être la source du miroir. mirror_last_synced=Dernière synchronisation @@ -625,6 +637,8 @@ migrate.lfs_mirror_unsupported=La synchronisation des objets LFS n'est pas suppo migrate.migrate_items_options=Quand vous migrez depuis github, saisissez un nom d'utilisateur et des options de migration seront affichées. migrated_from=Migré de %[2]s migrated_from_fake=Migré de %[1]s +migrate.migrating=Migration de %s ... +migrate.migrating_failed=La migration de %s a échoué. mirror_from=miroir de forked_from=bifurqué depuis @@ -673,6 +687,8 @@ stored_lfs=Stocké avec Git LFS commit_graph=Graphique des révisions blame=Annotations normal_view=Vue normale +line=ligne +lines=lignes editor.new_file=Nouveau fichier editor.upload_file=Téléverser un fichier @@ -698,6 +714,7 @@ editor.delete=Supprimer '%s' editor.commit_message_desc=Ajouter une description détaillée facultative… editor.commit_directly_to_this_branch=Soumettre directement dans la branche %s. editor.create_new_branch=Créer une nouvelle branche pour cette révision et envoyer une nouvelle demande d'ajout. +editor.create_new_branch_np=Créer une nouvelle branche pour cette révision. editor.propose_file_change=Proposer une modification du fichier editor.new_branch_name_desc=Nouveau nom de la branche… editor.cancel=Annuler @@ -973,6 +990,8 @@ pulls.cannot_merge_work_in_progress=Cette demande d'ajout est marquée comme en pulls.data_broken=Cette demande de fusion est impossible par manque d'informations de bifurcation. pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée. pulls.is_checking=Vérification des conflits de fusion en cours. Réessayez dans quelques instants. +pulls.required_status_check_failed=Certains contrôles requis n'ont pas réussi. +pulls.required_status_check_administrator=En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull. pulls.blocked_by_approvals=Cette demande d'ajout n'a pas assez d'approbation. %d sur %d approbations accordées. pulls.can_auto_merge_desc=Cette demande d'ajout peut être fusionnée automatiquement. pulls.cannot_auto_merge_desc=Cette demande de fusion ne peut être appliquée automatiquement en raison de conflits de fusion. @@ -980,6 +999,7 @@ pulls.cannot_auto_merge_helper=Fusionner manuellement pour résoudre les conflit pulls.no_merge_desc=Cette demande de fusion ne peut être appliquée directement car toutes les options de fusion du dépôt sont désactivées. pulls.no_merge_helper=Activez des options de fusion dans les paramètres du dépôt ou fusionnez la demande manuellement. pulls.no_merge_wip=Cette demande d'ajout ne peut pas être fusionnée car elle est marquée comme en cours de chantier. +pulls.no_merge_status_check=Cette demande de pull ne peut pas être fusionnée car tous les contrôles de statut requis ne sont pas réussis. pulls.merge_pull_request=Fusionner la demande d'ajout pulls.rebase_merge_pull_request=Rebase et fusionner pulls.rebase_merge_commit_pull_request=Rebase et Fusion (--no-ff) @@ -1121,6 +1141,7 @@ settings.collaboration=Collaborateurs settings.collaboration.admin=Administrateur settings.collaboration.write=Écriture settings.collaboration.read=Lecture +settings.collaboration.owner=Propriétaire settings.collaboration.undefined=Indéfini settings.hooks=Déclencheurs Web settings.githooks=Déclencheurs Git @@ -1129,7 +1150,9 @@ settings.mirror_settings=Réglages Miroir settings.sync_mirror=Synchroniser maintenant settings.mirror_sync_in_progress=La synchronisation est en cours. Revenez dans une minute. settings.email_notifications.enable=Activer les notifications par e-mail +settings.email_notifications.onmention=N'envoyer un e-mail que si vous êtes mentionné settings.email_notifications.disable=Désactiver les notifications par e-mail +settings.email_notifications.submit=Définir la préférence e-mail settings.site=Site Web settings.update_settings=Valider settings.advanced_settings=Paramètres avancés @@ -1200,6 +1223,11 @@ settings.collaborator_deletion_desc=La suppression d'un collaborateur révoque s settings.remove_collaborator_success=Le collaborateur a été retiré. settings.search_user_placeholder=Rechercher un utilisateur… settings.org_not_allowed_to_be_collaborator=Les organisations ne peuvent être ajoutées en tant que collaborateur. +settings.change_team_access_not_allowed=La modification de l'accès de l'équipe au dépôt a été limitée au propriétaire de l'organisation +settings.team_not_in_organization=L'équipe n'est pas dans la même organisation que le dépôt +settings.add_team_duplicate=L'équipe a déjà le dépôt +settings.add_team_success=L'équipe a maintenant accès au dépôt. +settings.remove_team_success=L'accès de l'équipe au dépôt a été supprimé. settings.add_webhook=Ajouter un déclencheur Web settings.add_webhook.invalid_channel_name=Le nom du canal Webhook ne peut pas être vide et ne peut pas contenir seulement un caractère #. settings.hooks_desc=Les Webhooks font automatiquement des requêtes HTTP POST à un serveur lorsque certains événements Gitea se déclenchent. Lire la suite dans le guide des Webhooks. @@ -1249,6 +1277,8 @@ settings.event_pull_request=Demande d'ajout settings.event_pull_request_desc=Demande d'ajout ouverte, fermée, réouverte, éditée, approuvée, rejetée, commentaire revu, assigné, non attribué, étiquette mise à jour, effacée ou synchronisée. settings.event_push=Pousser settings.event_push_desc=Git push vers un dépôt. +settings.branch_filter=Filtre de branche +settings.branch_filter_desc=Liste blanche pour les évènements de poussage, de création et de suppression de branche, spécifiés par un motif de développement. Si ce champ est vide ou vaut *, ces évènements sont rapportés pour toutes les branches . Voir la documentation pour la syntaxe sur github.com/gobwas/glob. Exemples : master, {master,release*}. settings.event_repository=Dépôt settings.event_repository_desc=Dépôt créé ou supprimé. settings.active=Actif @@ -1299,6 +1329,7 @@ settings.protect_merge_whitelist_committers=Activer la liste blanche pour la fus settings.protect_merge_whitelist_committers_desc=N'autoriser que les utilisateurs et les équipes en liste blanche d'appliquer les demandes de fusion sur cette branche. settings.protect_merge_whitelist_users=Utilisateurs en liste blanche de fusion : settings.protect_merge_whitelist_teams=Équipes en liste blanche de fusion : +settings.protect_check_status_contexts=Activer le Contrôle Qualité settings.protect_required_approvals=Agréments nécessaires : settings.protect_required_approvals_desc=N'autoriser la fusion qu'avec suffisamment de revues positives d'utilisateurs ou équipes sur liste blanche. settings.protect_approvals_whitelist_users=Réviseurs sur liste blanche : @@ -1335,7 +1366,6 @@ diff.parent=Parent diff.commit=révision diff.git-notes=Notes diff.data_not_available=Contenu de la comparaison indisponible -diff.show_diff_stats=Afficher les stats Diff diff.show_split_view=Vue séparée diff.show_unified_view=Vue unifiée diff.whitespace_button=Espace diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 05a83a8b07..2d90909d1e 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -1,4 +1,3 @@ - home=Főoldal dashboard=Műszerfal explore=Felfedezés @@ -50,6 +49,8 @@ issues=Hibajegyek cancel=Mégse +[startpage] + [install] install=Telepítés db_title=Adatbázis beállítások @@ -612,7 +613,6 @@ diff.browse_source=Forráskód böngészése diff.parent=szülő diff.commit=commit diff.data_not_available=A különbségek nem megjeleníthetőek -diff.show_diff_stats=Különbségstatisztika diff.show_split_view=Osztott nézet diff.show_unified_view=Egyesített nézet diff.stats_desc=%d fájl változott, egészen pontosan %d új sor hozzáadva és %d régi sor törölve diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index b7690f97b7..bd7d0eeb63 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -1,5 +1,3 @@ -app_desc=Sebuah layanan Git hosting pribadi yang mudah - home=Beranda dashboard=Dasbor explore=Jelajahi @@ -64,6 +62,8 @@ issues=Masalah cancel=Batal +[startpage] + [install] install=Pemasangan title=Konfigurasi Awal @@ -661,7 +661,6 @@ diff.browse_source=Telusuri Sumber diff.parent=orang tua diff.commit=melakukan diff.data_not_available=Konten Diff Tidak Tersedia -diff.show_diff_stats=Menunjukkan Perbedaan Statistik diff.show_split_view=Tampilan split diff.show_unified_view=Pandangan Terpadu diff.stats_desc= %d mengubah file dengan %d tambahan dan %d penghapusan diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 06044101de..7829406297 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1,5 +1,3 @@ -app_desc=Un servizio auto-ospitato per Git pronto all'uso - home=Home dashboard=Pannello di controllo explore=Esplora @@ -64,6 +62,8 @@ issues=Problemi cancel=Annulla +[startpage] + [install] install=Installazione title=Configurazione Iniziale @@ -1055,7 +1055,6 @@ diff.browse_source=Sfoglia il codice sorgente diff.parent=parent diff.commit=commit diff.data_not_available=Dati Diff non disponibili -diff.show_diff_stats=Mostra Diff Stats diff.show_split_view=Visualizzazione separata diff.show_unified_view=Visualizzazione unificata diff.stats_desc=%d ha cambiato i file con %d aggiunte e %d eliminazioni diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index ca38757e1c..f903f543e1 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1,5 +1,3 @@ -app_desc=痛みのない、自己ホスト型のGitサービス - home=ホーム dashboard=ダッシュボード explore=エクスプローラー @@ -75,6 +73,17 @@ write=書き込み preview=プレビュー loading=読み込み中… +[startpage] +app_desc=痛みのない、自己ホスト型のGitサービス +install=簡単インストール +install_desc=プラットフォームにあわせてバイナリを実行するだけ。 DockerVagrantでGiteaを出荷(シップ)することも、パッケージで入手することもできます。 +platform=クロスプラットフォーム +platform_desc=GiteaはGoでコンパイルできる環境ならどこでも動きます: Windows、macOS、Linux、ARM等々、お好みの環境でどうぞ! +lightweight=軽量 +lightweight_desc=Giteaの最小動作要件は低く、安価なRaspberry Piでも動きます。 電力を節約しよう! +license=オープンソース +license_desc=Go get code.gitea.io/gitea! コントリビュートして私たちと一緒にこのプロジェクトをより良くしていきましょう。 コントリビューターになるのに臆することはありません! + [install] install=インストール title=初期設定 @@ -250,7 +259,6 @@ openid_signin_desc=あなたのOpenID URIを入力してください。 例: htt disable_forgot_password_mail=アカウント回復機能は無効になっています。 サイト管理者にお問い合わせください。 email_domain_blacklisted=あなたのメールアドレスでは登録することはできません。 authorize_application=アプリケーションを許可 -authroize_redirect_notice=このアプリケーションを許可したときに %s にリダイレクトされます。 authorize_application_created_by=このアプリケーションは %s が作成しました。 authorize_application_description=アクセスを許可すると、このアプリケーションは、プライベート リポジトリや組織を含むあなたのすべてのアカウント情報に対して、アクセスと書き込みができるようになります。 authorize_title=%s"にあなたのアカウントへのアクセスを許可しますか? @@ -314,6 +322,7 @@ team_no_units_error=少なくともひとつのリポジトリセクションへ email_been_used=メールアドレスが既に使用されています。 openid_been_used=OpenIDのアドレス '%s' は既に使用されています。 username_password_incorrect=ユーザー名またはパスワードが間違っています。 +password_complexity=パスワードが複雑性の要件を満たしていません。 enterred_invalid_repo_name=入力したリポジトリ名が間違っています。 enterred_invalid_owner_name=新しいオーナーの名前が正しくありません。 enterred_invalid_password=入力されたパスワードが間違っています。 @@ -632,6 +641,8 @@ migrate.lfs_mirror_unsupported=LFSオブジェクトのミラーはサポート migrate.migrate_items_options=移行元がGitHubの場合は、ユーザー名を入力すると移行オプションが表示されます。 migrated_from=%[2]sから移行 migrated_from_fake=%[1]sから移行 +migrate.migrating=%s から移行しています ... +migrate.migrating_failed=%s からの移行が失敗しました。 mirror_from=ミラー元 forked_from=フォーク元 @@ -680,6 +691,8 @@ stored_lfs=Git LFSで保管されています commit_graph=コミットグラフ blame=Blame normal_view=通常表示 +line=行 +lines=行 editor.new_file=新規ファイル editor.upload_file=ファイルをアップロード @@ -705,6 +718,7 @@ editor.delete='%s' を削除 editor.commit_message_desc=詳細な説明を追加… editor.commit_directly_to_this_branch=ブランチ%sへ直接コミットする。 editor.create_new_branch=新しいブランチにコミットしてプルリクエストを作成する。 +editor.create_new_branch_np=新しいブランチにコミットする。 editor.propose_file_change=ファイル修正を提案 editor.new_branch_name_desc=新しいブランチ名… editor.cancel=キャンセル @@ -719,6 +733,8 @@ editor.file_editing_no_longer_exists=編集中のファイル '%s' が、もう editor.file_deleting_no_longer_exists=削除しようとしたファイル '%s' が、すでにリポジトリ内にありません。 editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 ここをクリックして何が変更されたか確認するか、もう一度"変更をコミット"をクリックして上書きします。 editor.file_already_exists=ファイル '%s' は、このリポジトリに既に存在します。 +editor.commit_empty_file_header=空ファイルのコミット +editor.commit_empty_file_text=コミットしようとしているファイルは空です。 続行しますか? editor.no_changes_to_show=表示する変更箇所はありません。 editor.fail_to_update_file=ファイル '%s' を作成または変更できませんでした: %v editor.add_subdir=ディレクトリを追加… @@ -1316,6 +1332,7 @@ settings.protect_this_branch=ブランチの保護を有効にする settings.protect_this_branch_desc=ブランチの削除を防ぎ、ブランチへのいかなるプッシュも無効にします。 settings.protect_whitelist_committers=プッシュ・ホワイトリストを有効にする settings.protect_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームに、このブランチへのプッシュを許可します。(強制プッシュ以外) +settings.protect_whitelist_deploy_keys=プッシュ可能な書き込み権限を持つデプロイキーをホワイトリストに含める settings.protect_whitelist_users=プッシュ・ホワイトリストに含むユーザー: settings.protect_whitelist_search_users=ユーザーを検索… settings.protect_whitelist_teams=プッシュ・ホワイトリストに含むチーム: @@ -1363,7 +1380,10 @@ diff.parent=親 diff.commit=コミット diff.git-notes=Notes diff.data_not_available=差分はありません -diff.show_diff_stats=差分情報を表示 +diff.options_button=差分オプション +diff.show_diff_stats=統計情報を表示 +diff.download_patch=Patchファイルをダウンロード +diff.download_diff=Diffファイルをダウンロード diff.show_split_view=分割表示 diff.show_unified_view=ユニファイド表示 diff.whitespace_button=空白 @@ -1446,6 +1466,8 @@ branch.restore_failed=ブランチ '%s' の復元に失敗しました。 branch.protected_deletion_failed=ブランチ '%s' は保護されています。 削除できません。 branch.restore=ブランチ '%s' の復元 branch.download=ブランチ '%s' をダウンロード +branch.included_desc=このブランチはデフォルトブランチに含まれています +branch.included=埋没 topic.manage_topics=トピックの管理 topic.done=完了 @@ -1962,12 +1984,15 @@ mark_as_unread=未読にする mark_all_as_read=すべて既読にする [gpg] +default_key=デフォルト鍵で署名 error.extract_sign=署名の抽出に失敗しました error.generate_hash=コミットのハッシュ生成に失敗しました error.no_committer_account=コミッターのメールアドレスに関連付けられたアカウントが存在しません error.no_gpg_keys_found=この署名に対応する既知のキーがデータベースに存在しません error.not_signed_commit=署名されたコミットではありません error.failed_retrieval_gpg_keys=コミッターのアカウントに登録されたキーを取得できませんでした +error.probable_bad_signature=警告! このIDの鍵はデータベースに登録されていますが、その鍵でコミットの検証が通りません! これは疑わしいコミットです。 +error.probable_bad_default_signature=警告! これはデフォルト鍵のIDですが、デフォルト鍵ではコミットの検証が通りません! これは疑わしいコミットです。 [units] error.no_unit_allowed_repo=このリポジトリのどのセクションにもアクセスが許可されていません。 diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index fe2f0c7fc2..f0ce4cdb30 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -1,4 +1,3 @@ - home=홈 dashboard=대시보드 explore=탐색 @@ -45,6 +44,8 @@ issues=이슈들 cancel=취소 +[startpage] + [install] install=설치 db_title=데이터베이스 설정 @@ -496,7 +497,6 @@ diff.browse_source=소스 검색 diff.parent=부모 diff.commit=커밋 diff.data_not_available=변경 데이터를 사용할 수 없습니다. -diff.show_diff_stats=변경상태 보기 diff.show_split_view=분할 보기 diff.show_unified_view=통합 보기 diff.stats_desc=%d개의 변경된 파일%d개의 추가작업 그리고 %d개의 파일을 삭제 diff --git a/options/locale/locale_lt-LT.ini b/options/locale/locale_lt-LT.ini index b0597cd1c3..b23bf5808d 100644 --- a/options/locale/locale_lt-LT.ini +++ b/options/locale/locale_lt-LT.ini @@ -1,4 +1,3 @@ - home=Pagrindinis dashboard=Skydelis explore=Naršyti @@ -38,6 +37,8 @@ issues=Problemos cancel=Atšaukti +[startpage] + [install] install=Diegimas db_title=Duombazės nustatymai diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index ff930d8da7..96fe30a7a4 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1,5 +1,3 @@ -app_desc=Viegli uzstādāms Git serviss - home=Sākums dashboard=Infopanelis explore=Izpētīt @@ -75,6 +73,8 @@ write=Rakstīt preview=Priekšskatītījums loading=Notiek ielāde… +[startpage] + [install] install=Instalācija title=Sākotnējā konfigurācija @@ -250,7 +250,6 @@ openid_signin_desc=Ievadiet savu OpenID URI, piemēram: https://anna.me, peteris disable_forgot_password_mail=Paroles atjaunošanas iespēja ir atslēgta. Sazinieties ar lapas administratoru. email_domain_blacklisted=Nav atļauts reģistrēties ar šādu e-pasta adresi. authorize_application=Autorizēt lietotni -authroize_redirect_notice=Jūs tiksiet nosūtīts uz %s, ja autorizēsiet šo lietotni. authorize_application_created_by=Šo lietotni izveidoja %s. authorize_application_description=Ja piešķirsiet tiesības, tā varēs piekļūt un mainīt Jūsu konta informāciju, ieskaitot privātos repozitorijus un organizācijas. authorize_title=Autorizēt "%s" piekļuvi jūsu kontam? @@ -299,6 +298,7 @@ max_size_error=` jabūt ne mazāk kā %s simbolu garumā.` email_error=` nav derīga e-pasta adrese.` url_error=` nav korekts URL.` include_error=` ir jāsatur tekstu '%s'.` +glob_pattern_error=` glob izteiksme nav korekta: %s.` unknown_error=Nezināma kļūda: captcha_incorrect=Ievadīts nepareizs drošības kods. password_not_match=Izvēlētā parole nesakrīt ar atkārtoti ievadīto. @@ -317,6 +317,7 @@ enterred_invalid_repo_name=Pārliecinieties, vai ievadītā repozitorija nosauku enterred_invalid_owner_name=Pārliecinieties, vai ievadītā īpašnieka vārds ir pareizs. enterred_invalid_password=Pārliecinieties, vai ievadītā parole ir pareiza. user_not_exist=Lietotājs neeksistē. +team_not_exist=Komanda neeksistē. last_org_owner=Nevar noņemt pēdējo īpašnieku komandas lietotāju, jo organizācijām ir jābūt vismaz vienam īpašniekam. cannot_add_org_to_team=Organizāciju nevar pievienot kā komandas biedru. @@ -577,6 +578,8 @@ fork_visibility_helper=Atdalītam repozitorijam nav iespējams mainīt tā redza repo_desc=Apraksts repo_lang=Valoda repo_gitignore_helper=Izvēlieties .gitignore sagatavi. +issue_labels=Problēmu etiķetes +issue_labels_helper=Izvēlieties problēmu etiķešu kopu. license=Licence license_helper=Izvēlieties licences failu. readme=LASIMANI @@ -628,6 +631,8 @@ migrate.lfs_mirror_unsupported=LFS objektu spoguļošana netiek atbalstīta - t migrate.migrate_items_options=Pārņemot datus no GitHub, ievadiet lietotāja vārdu, lai redzētu papildus iestatījumus. migrated_from=Migrēts no %[2]s migrated_from_fake=Migrēts no %[1]s +migrate.migrating=Migrācija no %s ... +migrate.migrating_failed=Migrācija no %s neizdevās. mirror_from=spogulis no forked_from=atdalīts no @@ -676,6 +681,8 @@ stored_lfs=Saglabāts Git LFS commit_graph=Revīziju grafs blame=Vainot normal_view=Parastais skats +line=rinda +lines=rindas editor.new_file=Jauna datne editor.upload_file=Augšupielādēt failu @@ -701,6 +708,7 @@ editor.delete=Dzēst '%s' editor.commit_message_desc=Pievienot neobligātu paplašinātu aprakstu… editor.commit_directly_to_this_branch=Apstiprināt revīzijas izmaiņas atzarā %s. editor.create_new_branch=Izveidot jaunu atzaru un izmaiņu pieprasījumu šai revīzijai. +editor.create_new_branch_np=Izveidot jaunu atzaru šai revīzijai. editor.propose_file_change=Ieteikt faila izmaiņas editor.new_branch_name_desc=Jaunā atzara nosaukums… editor.cancel=Atcelt @@ -832,6 +840,10 @@ issues.create_comment=Komentēt issues.closed_at=`aizvērts %[2]s` issues.reopened_at=`atvērts atkārtoti %[2]s` issues.commit_ref_at=`pieminēja šo problēmu revīzijā %[2]s` +issues.ref_issue_at=`atsaucās uz šo problēmu %[1]s` +issues.ref_pull_at=`atsaucās uz šo izmaiņu pieprasījumu %[1]s` +issues.ref_issue_ext_at=`atsaucās uz šo problēmu no %[1]s %[2]s` +issues.ref_pull_ext_at=`atsaucās uz šo izmaiņu pieprasījumu no %[1]s %[2]s` issues.poster=Autors issues.collaborator=Līdzstrādnieks issues.owner=Īpašnieks @@ -977,6 +989,8 @@ pulls.cannot_merge_work_in_progress=Šis izmaiņu pieprasījums ir atzīmēts, k pulls.data_broken=Izmaiņu pieprasījums ir bojāts, jo dzēsta informācija no atdalītā repozitorija. pulls.files_conflicted=Šīs izmaiņu pieprasījuma izmaiņas konfliktē ar mērķa atzaru. pulls.is_checking=Notiek konfliktu pārbaude, mirkli uzgaidiet un atjaunojiet lapu. +pulls.required_status_check_failed=Dažas no pārbaudēm nebija veiksmīgas. +pulls.required_status_check_administrator=Kā administrators Jūs varat sapludināt šo izmaiņu pieprasījumu. pulls.blocked_by_approvals=Šim izmaiņu pieprasījumam nav nepieciešamais apstiprinājumu daudzums. %d no %d apstiprinājumi piešķirti. pulls.can_auto_merge_desc=Šo izmaiņu pieprasījumu var automātiski sapludināt. pulls.cannot_auto_merge_desc=Šis izmaiņu pieprasījums nevar tikt automātiski sapludināts konfliktu dēļ. @@ -984,6 +998,7 @@ pulls.cannot_auto_merge_helper=Sapludiniet manuāli, lai atrisinātu konfliktus. pulls.no_merge_desc=Šo izmaiņu pieprasījumu nav iespējams sapludināt, jo nav atļauts neviens sapludināšanas veids. pulls.no_merge_helper=Lai sapludinātu šo izmaiņu pieprasījumu, iespējojiet vismaz vienu sapludināšanas veidu repozitorija iestatījumos vai sapludiniet to manuāli. pulls.no_merge_wip=Šo izmaiņu pieprasījumu nav iespējams sapludināt, jo tas ir atzīmēts, ka darbs pie tā vēl nav pabeigts. +pulls.no_merge_status_check=Šo izmaiņu pieprasījumu nevar saplusināt, jo nav veiksmīgi izildītas visas obligātas statusa pārbaudes. pulls.merge_pull_request=Izmaiņu pieprasījuma sapludināšana pulls.rebase_merge_pull_request=Pārbāzēt un sapludināt pulls.rebase_merge_commit_pull_request=Pārbāzēt un sapludināt (--no-ff) @@ -1125,6 +1140,7 @@ settings.collaboration=Līdzstrādnieks settings.collaboration.admin=Administrators settings.collaboration.write=Rakstīšanas settings.collaboration.read=Skatīšanās +settings.collaboration.owner=Īpašnieks settings.collaboration.undefined=Nedefinētas settings.hooks=Tīmekļa āķi settings.githooks=Git āķi @@ -1206,6 +1222,11 @@ settings.collaborator_deletion_desc=Noņemot līdzstrādnieku, tam tiks liegta p settings.remove_collaborator_success=Līdzstrādnieks tika noņemts. settings.search_user_placeholder=Meklēt lietotāju… settings.org_not_allowed_to_be_collaborator=Organizācijas nevar tikt pievienotas kā līdzstrādnieki. +settings.change_team_access_not_allowed=Iespēja mainīt komandu piekļuvi repozitorijam ir organizācijas īpašniekam +settings.team_not_in_organization=Komanda nav tajā pašā organizācijā kā repozitorijs +settings.add_team_duplicate=Komandai jau ir piekļuve šim repozitorijam +settings.add_team_success=Komandai tagad ir piekļuve šim repozitorijam. +settings.remove_team_success=Komandas piekļuve šim repozitorijam ir noņemta. settings.add_webhook=Pievienot tīmekļa āķi settings.add_webhook.invalid_channel_name=Tīmekļa āķa kanāla nosaukums nevar būt tukšs vai saturēt tikai # simbolu. settings.hooks_desc=Tīmekļa āķi ļauj paziņot ārējiem servisiem par noteiktiem notikumiem, kas notiek Gitea. Kad iestāsies kāds notikums, katram ārējā servisa URL tiks nosūtīts POST pieprasījums. Lai uzzinātu sīkāk skatieties tīmekļa āķu rokasgrāmatā. @@ -1255,6 +1276,8 @@ settings.event_pull_request=Izmaiņu pieprasījums settings.event_pull_request_desc=Izmaiņu pieprasījums izveidots, slēgts, atkārtoti atvērts, labots, apstiprināts, noraidīts, recenzēts, piešķirts, pievienots vai noņemts atbildīgais, pievienota etiķete, noņemta etiķete, pievienots vai noņemts atskaites punkts. settings.event_push=Izmaiņu nosūtīšana settings.event_push_desc=Git izmaiņu nosūtīšana uz repozitoriju. +settings.branch_filter=Atzaru filtrs +settings.branch_filter_desc=Atzaru ierobežojumi izmaiņu iesūtīšanas, zaru izveidošanas vai dzēšanas notikumien, izmantojot, glob izteiksmi. Ja norādīts tukšs vai *, notikumi uz visiem zariem tiks nosūtīti. Skatieties github.com/gobwas/glob pieraksta dokumentāciju. Piemērs: master, {master,release*}. settings.event_repository=Repozitorijs settings.event_repository_desc=Repozitorijs izveidots vai dzēsts. settings.active=Aktīvs @@ -1305,6 +1328,9 @@ settings.protect_merge_whitelist_committers=Iespējot sapludināšanas ierobežo settings.protect_merge_whitelist_committers_desc=Atļaut tikai noteiktiem lietotājiem vai komandām sapludināt izmaiņu pieprasījumus šajā atzarā. settings.protect_merge_whitelist_users=Lietotāji, kas var veikt izmaiņu sapludināšanu: settings.protect_merge_whitelist_teams=Komandas, kas var veikt izmaiņu sapludināšanu: +settings.protect_check_status_contexts=Iespējot statusu pārbaudi +settings.protect_check_status_contexts_desc=Nepieciešamas veiksmīgas statusa pārbaudes pirms sapludināšanas. Izvēlieties, kurām statusa pārbaudēm ir jāizpildās pirms ir iespejams tās sapludināt. Ja iespējots, tad revīzijas sākotnēji jānosūta uz atsevišķu atzaru, pēc kā var tikt saplusinātas vai tieši nosūtītas uz atzariem, kas atbildst veiksmīgām norādītajām stautsa pārbaudēm. Ja konteksts nav norādīts, pēdējai revīzijai ir jābūt veiksmīga neatkarīgi no konteksta. +settings.protect_check_status_contexts_list=Statusu pārbaudes, kas šim repozitorijam bijušas pēdējās nedēļas laikā settings.protect_required_approvals=Vajadzīgi apstiprinājumi: settings.protect_required_approvals_desc=Atļaut tikai noteiktiem lietotājiem vai komandām sapludināt izmaiņu pieprasījumu, kam veikts noteikts daudzums pozitīvu recenziju. settings.protect_approvals_whitelist_users=Lietotāji, kas var veikt recenzijas: @@ -1341,7 +1367,6 @@ diff.parent=vecāks diff.commit=revīzija diff.git-notes=Piezīmes diff.data_not_available=Satura salīdzināšana nav pieejama -diff.show_diff_stats=Rādīt salīdzināšanas statistiku diff.show_split_view=Dalītais skats diff.show_unified_view=Apvienotais skats diff.whitespace_button=Atstarpes @@ -1352,6 +1377,11 @@ diff.whitespace_ignore_at_eol=Ignorēt atstarpju izmaiņas rindu beigās diff.stats_desc=%d mainītis faili ar %d papildinājumiem un %d dzēšanām diff.bin=Binārs diff.view_file=Parādīt failu +diff.file_before=Pirms +diff.file_after=Pēc +diff.file_image_width=Platums +diff.file_image_height=Augstums +diff.file_byte_size=Izmērs diff.file_suppressed=Failā izmaiņas netiks attēlotas, jo tās ir par lielu diff.too_many_files=Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels diff.comment.placeholder=Ievadiet komentāru @@ -1454,6 +1484,8 @@ settings.options=Organizācija settings.full_name=Pilns vārds, uzvārds settings.website=Mājas lapa settings.location=Atrašanās vieta +settings.permission=Tiesības +settings.repoadminchangeteam=Repozitorija administrators var pievienot vain noņemt piekļuvi komandām settings.visibility=Redzamība settings.visibility.public=Publiska settings.visibility.limited=Ierobežota (redzama tikai autorizētiem lietotājiem) @@ -1700,6 +1732,7 @@ auths.tip.google_plus=Iegūstiet OAuth2 klienta pilnvaru no Google API konsoles auths.tip.openid_connect=Izmantojiet OpenID pieslēgšanās atklāšanas URL (/.well-known/openid-configuration), lai norādītu galapunktus auths.tip.twitter=Dodieties uz adresi https://dev.twitter.com/apps, izveidojiet aplikāciju un pārliecinieties, ka ir atzīmēts “Allow this application to be used to Sign in with Twitter” auths.tip.discord=Reģistrējiet jaunu aplikāciju adresē https://discordapp.com/developers/applications/me +auths.tip.gitea=Reģistrēt jaunu OAuth2 lietojumprogrammu. Pamācību iespējams atrast https://docs.gitea.io/en-us/oauth2-provider/ auths.edit=Labot autentifikācijas avotu auths.activated=Autentifikācijas avots ir atkivizēts auths.new_success=Jauna autentifikācija'%s' tika pievienota. diff --git a/options/locale/locale_ml-IN.ini b/options/locale/locale_ml-IN.ini index f2571ce9c0..35af054c15 100644 --- a/options/locale/locale_ml-IN.ini +++ b/options/locale/locale_ml-IN.ini @@ -6,6 +6,7 @@ +[startpage] [install] diff --git a/options/locale/locale_nb-NO.ini b/options/locale/locale_nb-NO.ini index 763bfec6a6..d83f5829de 100644 --- a/options/locale/locale_nb-NO.ini +++ b/options/locale/locale_nb-NO.ini @@ -1,5 +1,3 @@ -app_desc=En smertefri Git-tjeneste du kan kjøre selv - home=Startside dashboard=Skrivebord explore=Utforsk @@ -75,6 +73,8 @@ write=Skriv preview=Forhåndsvis loading=Laster inn… +[startpage] + [install] install=Installasjon title=Standard konfigurasjon diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 92ea18dd91..66f8efe031 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1,5 +1,3 @@ -app_desc=Een eenvoudige, self-hosted Git service - home=Beginscherm dashboard=Overzicht explore=Verkennen @@ -75,6 +73,8 @@ write=Schrijf preview=Voorbeeld loading=Laden… +[startpage] + [install] install=Installatie title=Initiële configuratie @@ -1034,7 +1034,6 @@ diff.browse_source=Bladeren bron diff.parent=bovenliggende diff.commit=commit diff.data_not_available=Diff gegevens niet beschikbaar -diff.show_diff_stats=Toon Diff Stats diff.show_split_view=Zij-aan-zij weergave diff.show_unified_view=Gecombineerde weergave diff.whitespace_button=Witregel diff --git a/options/locale/locale_nn-NO.ini b/options/locale/locale_nn-NO.ini index f2571ce9c0..35af054c15 100644 --- a/options/locale/locale_nn-NO.ini +++ b/options/locale/locale_nn-NO.ini @@ -6,6 +6,7 @@ +[startpage] [install] diff --git a/options/locale/locale_no-NO.ini b/options/locale/locale_no-NO.ini index 79198d129b..326ee445e5 100644 --- a/options/locale/locale_no-NO.ini +++ b/options/locale/locale_no-NO.ini @@ -1,4 +1,3 @@ - home=Startside dashboard=Skrivebord explore=Utforsk @@ -36,6 +35,8 @@ issues=Problemer cancel=Avbryt +[startpage] + [install] install=Installasjon db_title=Databaseinnstillinger diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 52cddf4735..cf8d8ed59c 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1,5 +1,3 @@ -app_desc=Bezbolesna usługa Git na własnym serwerze - home=Strona główna dashboard=Pulpit explore=Odkrywaj @@ -75,6 +73,8 @@ write=Napisz preview=Podgląd loading=Ładowanie… +[startpage] + [install] install=Instalacja title=Wstępna konfiguracja @@ -250,7 +250,6 @@ openid_signin_desc=Wpisz swój URI OpenID. Na przykład: https://anne.me, bob.op disable_forgot_password_mail=Odzyskiwanie konta jest wyłączone. Skontaktuj się z administratorem strony. email_domain_blacklisted=Nie możesz zarejestrować się za pomocą tego adresu e-mail. authorize_application=Autoryzuj aplikację -authroize_redirect_notice=Zostaniesz przekierowany(-a) do %s, jeśli autoryzujesz tę aplikację. authorize_application_created_by=Ta aplikacja została stworzona przez %s. authorize_application_description=Jeżeli udzielisz dostępu, aplikacja uzyska dostęp z zapisem do wszystkich informacji o Twoim koncie, wraz z prywatnymi repozytoriami i organizacjami. authorize_title=Zezwolić "%s" na dostęp do Twojego konta? @@ -1303,7 +1302,6 @@ diff.parent=rodzic diff.commit=commit diff.git-notes=Notatki diff.data_not_available=Informacje nt. zmian nie są dostępne -diff.show_diff_stats=Pokaż statystyki zmian diff.show_split_view=Widok podzielony diff.show_unified_view=Zunifikowany widok diff.whitespace_button=Znaki białe @@ -1797,7 +1795,7 @@ notices.delete_success=Powiadomienia systemu zostały usunięte. [action] create_repo=tworzy repozytorium %s rename_repo=zmienia nazwę repozytorium %[1]s na %[3]s -commit_repo=wypycha do %[3]s w[4]s +commit_repo=wypycha do %[3]s w%[4]s create_issue=`otwiera zgłoszenie %s#%[2]s` close_issue=`zamyka zgłoszenie %s#%[2]s` reopen_issue=`ponownie otwiera zgłoszenie %s#%[2]s` diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 66c2d9fd4c..30549ba69d 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1,5 +1,3 @@ -app_desc=Um serviço de hospedagem Git amigável - home=Página inicial dashboard=Painel explore=Explorar @@ -75,6 +73,17 @@ write=Escrever preview=Pré-visualização loading=Carregando… +[startpage] +app_desc=Um serviço de hospedagem Git amigável +install=Fácil de instalar +install_desc=Simplesmente rode o executável para o seu sistema operacional. Ou obtenha o Gitea com o Docker ou Vagrant, ou baixe o pacote. +platform=Multi-plataforma +platform_desc=Gitea roda em qualquer sistema operacional em que Go consegue compilar: Windows, macOS, Linux, ARM, etc. Escolha qual você gosta mais! +lightweight=Leve e rápido +lightweight_desc=Gitea utiliza poucos recursos e consegue mesmo rodar no barato Raspberry Pi. Economize energia elétrica da sua máquina! +license=Código aberto +license_desc=Está tudo no GitHub! Contribua e torne este projeto ainda melhor. Não tenha vergonha de contribuir! + [install] install=Instalação title=Configuração inicial @@ -250,7 +259,7 @@ openid_signin_desc=Digite a URI do seu OpenID. Por exemplo: https://anne.me, bob disable_forgot_password_mail=Recuperação de conta está desativada. Por favor, contate o administrador do servidor. email_domain_blacklisted=Você não pode se cadastrar com seu endereço de e-mail. authorize_application=Autorizar aplicativo -authroize_redirect_notice=Você será redirecionado para %s se você autorizar este aplicativo. +authorize_redirect_notice=Você será redirecionado para %s se você autorizar este aplicativo. authorize_application_created_by=Este aplicativo foi criado por %s. authorize_application_description=Se você conceder o acesso, ele será capaz de acessar e escrever em todas as informações da sua conta, incluindo repositórios privados e organizações. authorize_title=Autorizar "%s" para acessar sua conta? @@ -314,6 +323,7 @@ team_no_units_error=Permitir acesso a pelo menos uma seção de repositório. email_been_used=Este endereço de e-mail já está sendo usado. openid_been_used=O endereço OpenID '%s' já está sendo usado. username_password_incorrect=Nome de usuário ou senha incorretos. +password_complexity=A senha não passa nos requisitos de complexidade. enterred_invalid_repo_name=O nome do repositório que você digitou está incorreto. enterred_invalid_owner_name=O nome do novo proprietário não é válido. enterred_invalid_password=A senha que você digitou está incorreta. @@ -632,6 +642,8 @@ migrate.lfs_mirror_unsupported=Espelhamento de objetos Git LFS não é suportado migrate.migrate_items_options=Ao migrar do github, insira um nome de usuário e as opções de migração serão exibidas. migrated_from=Migrado de %[2]s migrated_from_fake=Migrado de %[1]s +migrate.migrating=Migrando a partir de %s ... +migrate.migrating_failed=Migração a partir de %s falhou. mirror_from=espelhamento de forked_from=feito fork de @@ -680,6 +692,8 @@ stored_lfs=Armazenado com Git LFS commit_graph=Gráfico de commits blame=Anotar normal_view=Visão normal +line=linha +lines=linhas editor.new_file=Novo arquivo editor.upload_file=Enviar arquivo @@ -705,6 +719,7 @@ editor.delete=Excluir '%s' editor.commit_message_desc=Adicione uma descrição detalhada (opcional)... editor.commit_directly_to_this_branch=Commit diretamente no branch %s. editor.create_new_branch=Crie um novo branch para este commit e crie um pull request. +editor.create_new_branch_np=Crie um novo branch para este commit. editor.propose_file_change=Propor alteração de arquivo editor.new_branch_name_desc=Novo nome do branch... editor.cancel=Cancelar @@ -719,6 +734,8 @@ editor.file_editing_no_longer_exists=O arquivo que está sendo editado, '%s', n 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=Houve erro ao criar ou atualizar arquivo '%s': %v editor.add_subdir=Adicionar um subdiretório... @@ -784,6 +801,7 @@ issues.delete_branch_at=`excluiu branch %s %s` issues.open_tab=%d aberto issues.close_tab=%d fechado issues.filter_label=Etiqueta +issues.filter_label_exclude=`Use alt + clique/enter para excluir etiquetas` issues.filter_label_no_select=Todas as etiquetas issues.filter_milestone=Marco issues.filter_milestone_no_select=Todos os marcos @@ -1316,6 +1334,7 @@ settings.protect_this_branch=Habilitar proteção de branch settings.protect_this_branch_desc=Prevenir exclusão e desabilitar qualquer push neste branch. settings.protect_whitelist_committers=Habilitar controle de permissão de push settings.protect_whitelist_committers_desc=Permitir que usuários ou times realizem push neste branch (exceto push forçado). +settings.protect_whitelist_deploy_keys=Lista de chaves de implantação com acesso de escrita para realizar push settings.protect_whitelist_users=Usuários com permissão para realizar push: settings.protect_whitelist_search_users=Pesquisar usuários... settings.protect_whitelist_teams=Equipes com permissão para realizar push: @@ -1363,7 +1382,10 @@ diff.parent=pai diff.commit=commit diff.git-notes=Notas diff.data_not_available=Conteúdo de diff não disponível -diff.show_diff_stats=Mostrar estatísticas do Diff +diff.options_button=Opções de diferenças +diff.show_diff_stats=Mostrar estatísticas +diff.download_patch=Baixar arquivo de patch +diff.download_diff=Baixar arquivo de diferenças diff.show_split_view=Visão dividida diff.show_unified_view=Visão unificada diff.whitespace_button=Espaço em branco @@ -1446,6 +1468,8 @@ branch.restore_failed=Falha ao restaurar a branch %s. branch.protected_deletion_failed=A branch '%s' está protegida. Ela não pode ser excluída. branch.restore=Restaurar branch '%s' branch.download=Baixar branch '%s' +branch.included_desc=Este branch faz parte do branch padrão +branch.included=Incluído topic.manage_topics=Gerenciar Tópicos topic.done=Feito @@ -1962,12 +1986,15 @@ mark_as_unread=Marcar como não lida mark_all_as_read=Marcar todas como lidas [gpg] +default_key=Assinado com a chave padrão error.extract_sign=Falha ao extrair assinatura error.generate_hash=Falha ao gerar hash de commit error.no_committer_account=Nenhuma conta vinculada ao e-mail do autor do commit error.no_gpg_keys_found=Nenhuma chave conhecida encontrada para esta assinatura no banco de dados error.not_signed_commit=Não é um commit assinado error.failed_retrieval_gpg_keys=Falha em obter qualquer chave anexada à conta do autor do commit +error.probable_bad_signature=AVISO! Embora exista uma chave com este ID no banco de dados, ela não verifica este commit! Este commit é SUSPEITO. +error.probable_bad_default_signature=AVISO! Embora a chave padrão tenha este ID, ela não verifica este commit! Este commit é SUSPEITO. [units] error.no_unit_allowed_repo=Você não tem permissão para acessar nenhuma seção deste repositório. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 475129ce36..9102603425 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1,5 +1,3 @@ -app_desc=Удобная служба для собственного Git-репозитория - home=Главная dashboard=Панель управления explore=Обзор @@ -75,6 +73,8 @@ write=Редактирование preview=Предпросмотр loading=Загрузка… +[startpage] + [install] install=Установка title=Начальная конфигурация @@ -245,7 +245,6 @@ openid_register_desc=Выбранный OpenID URI неизвестен. Свя openid_signin_desc=Введите свой OpenID URI. Например: https://anne.me, bob.openid.org.cn или gnusocial.net/carry. disable_forgot_password_mail=Восстановление аккаунта отключено. Пожалуйста, свяжитесь с администратором сайта. authorize_application=Авторизация приложения -authroize_redirect_notice=Вы будете перенаправлены на %s, если вы авторизуете это приложение. authorize_application_created_by=Это приложение было создано %s. authorize_application_description=Если вы предоставите доступ, оно сможет получить доступ и редактировать любую информацию о вашей учетной записи, включая содержимое частных репозиториев и организаций. authorize_title=Разрешить «%s» доступ к вашей учетной записи? @@ -1265,7 +1264,6 @@ diff.browse_source=Просмотр исходного кода diff.parent=Родитель diff.commit=Сommit diff.data_not_available=Разница недоступна -diff.show_diff_stats=Показать статистику Diff diff.show_split_view=Разделённый вид diff.show_unified_view=Единый вид diff.whitespace_button=Пробелы diff --git a/options/locale/locale_sr-SP.ini b/options/locale/locale_sr-SP.ini index 8b0d5743fb..a4072b7dfa 100644 --- a/options/locale/locale_sr-SP.ini +++ b/options/locale/locale_sr-SP.ini @@ -1,4 +1,3 @@ - home=Почетна dashboard=Контролни панел explore=Преглед @@ -36,6 +35,8 @@ issues=Дискусије cancel=Откажи +[startpage] + [install] install=Инсталација db_title=Подешавања базе @@ -418,7 +419,6 @@ settings.deploy_key_content=Садржај diff.browse_source=Преглед изворни кода diff.parent=родитељ diff.commit=комит -diff.show_diff_stats=Покажи статистику Diff diff.show_split_view=Подељен поглед diff.show_unified_view=Један поглед diff.stats_desc= %d измењених фајлова са %d додато и %d уклоњено diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index a954fd2a9a..37d6621642 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -1,5 +1,3 @@ -app_desc=En smidig, självhostad Git-tjänst - home=Startsida dashboard=Instrumentpanel explore=Utforska @@ -71,6 +69,8 @@ cancel=Avbryt preview=Förhandsgranska loading=Laddar… +[startpage] + [install] install=Installation title=Ursprunglig konfiguration @@ -1086,7 +1086,6 @@ diff.browse_source=Bläddra i källkod diff.parent=förälder diff.commit=incheckning diff.data_not_available=Diff Content ej tillgänglig -diff.show_diff_stats=Visa Diff Statistik diff.show_split_view=Delad Vy diff.show_unified_view=Unifierad Vy diff.whitespace_show_everything=Visa alla ändringar diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 9fa1b7d42b..5100f84319 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1,5 +1,3 @@ -app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git hizmeti - home=Ana Sayfa dashboard=Pano explore=Keşfet @@ -66,7 +64,7 @@ forks=Çatallar activities=Aktiviteler pull_requests=Değişiklik İsteği -issues=Sorunlar +issues=Konular cancel=İptal @@ -74,6 +72,16 @@ write=Yaz preview=Önizleme loading=Yükleniyor… +[startpage] +app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi +install=Kurulumu kolay +install_desc=Basitçe, platformunuz için uygun program dosyasını çalıştırın. Ya da Gitea'yı Docker veya Vagrant ile sunun, ya da paketlenmiş olarak edinin. +platform=Farklı platformlarda çalışablir +platform_desc=Gitea Go ile derleme yapılabilecek her yerde çalışmaktadır: Windows, macOS, Linux, ARM, vb. Hangisini seviyorsanız onu seçin! +lightweight=Hafif +lightweight_desc=Gitea'nın minimal gereksinimleri çok düşüktür ve ucuz bir Raspberry Pi üzerinde çalışabilmektedir. Makine enerjinizden tasarruf edin! +license=Açık Kaynak + [install] install=Kurulum title=Başlangıç Yapılandırması @@ -112,6 +120,7 @@ domain_helper=SSH kopyalama URL'leri için alan adı veya sunucu adresi. ssh_port=SSH Sunucu Portu http_port=Gitea HTTP Dinleme Portu http_port_helper=Gitea'nın web sunucusunun dinleyeceği port numarası. +app_url=Gitea Kök URL log_root_path=Günlük Dosyaları Yolu log_root_path_helper=Günlük dosyaları bu dizine kaydedilecektir. @@ -124,6 +133,7 @@ mailer_user=SMTP Kullanıcı Adı mailer_password=SMTP Parolası register_confirm=Kayıt için E-posta Doğrulaması Gereksin mail_notify=E-Posta Bildirimlerini Etkinleştir +server_service_title=Sunucu ve Diğer Servis Ayarları offline_mode=Yerel Kipi Etkinleştir disable_gravatar=Gravatar'ı Devre Dışı Bırak federated_avatar_lookup_popup=Enable federated avatars lookup to use federated open source service based on libravatar. @@ -149,6 +159,7 @@ test_git_failed='git' komut testi başarısız: %v sqlite3_not_available=Bu Gieta sürümü SQLite3 desteklemiyor. Lütfen %s adresinden resmi çalışır sürümü ('gobuild' sürümünü değil) indirin. invalid_db_setting=Veritabanı ayarları geçersiz: %v invalid_repo_path=Depo kök dizini geçersiz: %v +run_user_not_match='Birlikte çalıştır' kullanıcı adı şimdiki kullanıcı adından farklıdır: %s -> %s save_config_failed=%v Yapılandırması kaydedilirken hata oluştu invalid_admin_setting=Yönetici hesap ayarları geçersiz: %v install_success=Hoşgeldiniz! Gitea'yı seçtiğiniz için teşekkür ederiz. Eğlenin ve kendinize iyi bakın! @@ -157,6 +168,8 @@ default_keep_email_private=E-posta adreslerini varsayılan olarak gizle default_keep_email_private_popup=Yeni kullanıcı hesaplarının e-posta adreslerini varsayılan olarak gizle. default_allow_create_organization=Varsayılan Olarak Organizasyon Oluşturmaya İzin Ver default_allow_create_organization_popup=Varsayılan olarak yeni kullanıcı hesaplarının organizasyon oluşturmasına izin ver. +default_enable_timetracking=Varsayılan Olarak Zaman Takibini Etkinleştir +default_enable_timetracking_popup=Yeni depolar için zaman takibini varsayılan olarak etkinleştir. no_reply_address=Gizlenecek E-Posta Alan Adı no_reply_address_helper=Gizlenmiş e-posta adresine sahip kullanıcılar için alan adı. Örneğin 'ali' kullanıcı adı, gizlenmiş e-postalar için alan adı 'yanityok.ornek.org' olarak ayarlandığında Git günlüğüne 'ali@yanityok.ornek.org' olarak kaydedilecektir. @@ -203,6 +216,8 @@ allow_password_change=Kullanıcıyı parola değiştirmeye zorla (önerilen) reset_password_mail_sent_prompt=%s adresine bir onay e-postası gönderildi. Hesap kurtarma işlemini tamamlamak için lütfen gelen kutunuzu sonraki %s içinde kontrol edin. active_your_account=Hesabınızı Aktifleştirin account_activated=Hesap etkinleştirildi +prohibit_login=Oturum Açma Yasağı +prohibit_login_desc=Hesabınız ile oturum açmanız yasaklanmış, lütfen site yöneticinizle iletişime geçin. resent_limit_prompt=Zaten bir doğrulama e-postası talep ettiniz. Lütfen 3 dakika bekleyip tekrar deneyin. has_unconfirmed_mail=Merhaba %s, doğrulanmamış bir e-posta adresin var (%s). Bir doğrulama e-postası almadıysanız ya da yenisine ihtiyacınız varsa lütfen aşağıdaki düğmeye tıklayın. resend_mail=Doğrulama e-postasını tekrar almak için buraya tıklayın @@ -235,7 +250,6 @@ openid_register_desc=Seçilen OpenID URI'si bilinmiyor. Burada yeni bir hesapla disable_forgot_password_mail=Hesap kurtarma devre dışı. Lütfen site yöneticinizle iletişime geçin. email_domain_blacklisted=Bu e-posta adresinizle kayıt olamazsınız. authorize_application=Uygulamayı Yetkilendir -authroize_redirect_notice=Bu uygulamayı yetkilendirirseniz %s adresine yönlendirileceksiniz. authorize_application_created_by=Bu uygulama %s tarafından oluşturuldu. authorize_application_description=Erişime izin verirseniz, özel depolar ve organizasyonlar da dahil olmak üzere tüm hesap bilgilerinize erişebilir ve yazabilir. authorize_title=Hesabınıza erişmesi için "%s" yetkilendirilsin mi? @@ -455,11 +469,11 @@ unbind=Bağlantıyı Kaldır unbind_success=Sosyal hesabın bağlantısı Gitea hesabınızdan kaldırılmıştır. manage_access_token=Erişim Jetonlarını Yönet -generate_new_token=Yeni Erişim Anahtarı Üret +generate_new_token=Yeni Jeton Üret tokens_desc=Bu jetonlar Gitea API'sini kullanarak hesabınıza erişim sağlar. new_token_desc=Jeton kullanan uygulamalar hesabınıza tam erişime sahiptir. -token_name=Erişim Anahtarı İsmi -generate_token=Erişim Anahtarı Üret +token_name=Jeton İsmi +generate_token=Jeton Üret generate_token_success=Yeni bir jeton oluşturuldu. Tekrar gösterilmeyeceği için şimdi kopyalayın. delete_token=Sil access_token_deletion=Erişim Jetonunu Sil @@ -585,7 +599,10 @@ migrate.invalid_local_path=Yerel yol geçersiz. Mevcut değil veya bir dizin de migrate.failed=Göç başarısız: %v migrate.lfs_mirror_unsupported=LFS nesnelerini yansılama desteklenmiyor - yerine 'git lfs fetch --all' ve 'git lfs push --all' kullanın. migrate.migrate_items_options=Github'dan göç yaparken, bir kullanıcı adı girin ve göç seçenekleri görüntülenecektir. +migrated_from=%[2]s konumundan taşındı migrated_from_fake=%[1]s Konumundan Taşındı +migrate.migrating=%s konumundan taşınıyor ... +migrate.migrating_failed=%s konumundan taşıma başarısız oldu. mirror_from=şunun yansıması forked_from=şundan çatallanmış @@ -616,7 +633,7 @@ tree=Ağaç filter_branch_and_tag=Dal veya biçim imini filtrele branches=Dallar tags=Biçim İmleri -issues=Sorunlar +issues=Konular pulls=Değişiklik İstekleri labels=Etiketler milestones=Kilometre Taşları @@ -632,13 +649,16 @@ video_not_supported_in_browser=Tarayıcınız HTML5 'video' etiketini desteklemi audio_not_supported_in_browser=Tarayıcınız HTML5 'audio' etiketini desteklemiyor. stored_lfs=Git LFS ile depolandı commit_graph=İşleme Grafiği -blame=Bahset +blame=Suçlama normal_view=Normal Görünüm +line=satır +lines=satır editor.new_file=Yeni dosya editor.upload_file=Dosya Yükle editor.edit_file=Dosyayı Düzenle editor.preview_changes=Değişiklikleri Önizle +editor.cannot_edit_lfs_files=LFS dosyaları web arayüzünde düzenlenemez. editor.cannot_edit_non_text_files=Bu tür dosyalar web arayüzünden düzenlenemez. editor.edit_this_file=Dosyayı Düzenle editor.must_be_on_a_branch=Bu dosyada değişiklik yapmak veya önermek için bir dalda olmalısınız. @@ -657,6 +677,7 @@ editor.delete='%s' sil editor.commit_message_desc=İsteğe bağlı uzun bir açıklama ekleyin… editor.commit_directly_to_this_branch=Doğrudan %s bölümüne uygula. editor.create_new_branch=Bu işlem için bir yeni branş oluşturun ve bir çekme istediği başlatın. +editor.create_new_branch_np=Bu işleme için yeni bir dal oluştur. editor.propose_file_change=Dosya değişikliği öner editor.new_branch_name_desc=Yeni dal ismi… editor.cancel=İptal @@ -671,6 +692,8 @@ editor.file_editing_no_longer_exists=Düzenlenmekte olan '%s' dosyası artık bu editor.file_deleting_no_longer_exists=Silinen '%s' dosyası bu depoda artık yer almıyor değil. editor.file_changed_while_editing=Düzenlemeye başladığınızdan beri dosya içeriği değişti. Görmek için burayı tıklayın veya üzerine yazmak için değişiklikleri yine de işleyin. editor.file_already_exists=Bu depoda '%s' isimli bir dosya zaten mevcut. +editor.commit_empty_file_header=Boş bir dosya işle +editor.commit_empty_file_text=İşleme yaptığınız dosya boş. Devam edilsin mi? editor.no_changes_to_show=Gösterilecek değişiklik yok. editor.fail_to_update_file=Şu hata ile '%s' dosyasını güncelleme/oluşturma başarısız oldu: %v editor.add_subdir=Bir dizin ekle… @@ -696,7 +719,7 @@ ext_issues=Dışsal Konular ext_issues.desc=Dışsal konu takip sistemine bağla. issues.desc=Hata raporlarını, görevleri ve kilometre taşlarını yönetmenizi sağlar. -issues.new=Yeni Sorun +issues.new=Yeni Konu issues.new.title_empty=Başlık boş olamaz issues.new.labels=Etiketler issues.new.no_label=Etiket Yok @@ -710,7 +733,7 @@ issues.new.assignees=Atananlar issues.new.clear_assignees=Atamaları Temizle issues.new.no_assignees=Atanan Kişi Yok issues.no_ref=Bölüm/Etiket Belirtilmedi -issues.create=Sorun Oluştur +issues.create=Konu Oluştur issues.new_label=Yeni Etiket issues.new_label_placeholder=Etiket adı issues.new_label_desc_placeholder=Açıklama @@ -720,28 +743,29 @@ issues.label_templates.info=Henüz bir etiket yok. 'Yeni Etiket' ile bir etiket issues.label_templates.helper=Bir etiket seti seçin issues.label_templates.use=Etiket Kümesi Kullan issues.label_templates.fail_to_load_file=Etiket şablon dosyası yüklemesi başarısız oldu '%s':%v -issues.add_label_at=
%s
etiketi eklendi %s +issues.add_label_at= %[4]s
%[3]s
etiketini eklendi issues.remove_label_at=
%s
etiketi silindi %s -issues.add_milestone_at=`bu %s yol taşına eklendi %s` +issues.add_milestone_at=`%[2]s %[1]s kilometre taşına ekledi` issues.change_milestone_at=` %sden%sye yol taşı düzenlendi %s` issues.remove_milestone_at=`bu dosya %s yol taşından kaldırıldı %s` issues.deleted_milestone=`(silindi)` -issues.self_assign_at=`kendiliğinden atanmış bu %s` -issues.add_assignee_at=`%s tarafından atandı %s` -issues.remove_assignee_at=`%s tarafından atama kaldırıldı %s` +issues.self_assign_at=`%s kendini atadı` +issues.add_assignee_at=`%[2]s %[1]s tarafından atandı` +issues.remove_assignee_at=`ataması %[2]s %[1]s tarafından kaldırıldı` issues.remove_self_assignment=`atamalarını kaldırdı %s` issues.change_title_at=`başlık %s iken %s olarak değiştirildi %s` issues.delete_branch_at=`branş %s silindi %s` issues.open_tab=%d açık issues.close_tab=%d kapanmış issues.filter_label=Etiket +issues.filter_label_exclude=`Etiketleri hariç tutmak için alt + tıkla/enter kullanın` issues.filter_label_no_select=Tüm etiketler issues.filter_milestone=Kilometre Taşı issues.filter_milestone_no_select=Tüm kilometre taşları issues.filter_assignee=Atanan issues.filter_assginee_no_select=Tüm atananlar issues.filter_type=Tür -issues.filter_type.all_issues=Tüm Sorunlar +issues.filter_type.all_issues=Tüm Konular issues.filter_type.assigned_to_you=Size atanan issues.filter_type.created_by_you=Sizin oluşturduklarınız issues.filter_type.mentioning_you=Sizden bahsedilen @@ -766,6 +790,8 @@ issues.action_milestone_no_select=Kilometre Taşı Yok issues.action_assignee=Vekil issues.action_assignee_no_select=Vekil yok issues.opened_by=%[3]s tarafından %[1]s açıldı +pulls.merged_by=%[3]s tarafından %[1]s birleştirildi +pulls.merged_by_fake=%[2]s tarafından %[1]s birleştirildi issues.closed_by=%[3]s tarafından %[1]s kapatıldı issues.opened_by_fake=%[2]s tarafından %[1]s açıldı issues.closed_by_fake=%[2]s tarafından %[1]s kapatıldı @@ -774,7 +800,7 @@ issues.next=Sonraki issues.open_title=Açık issues.closed_title=Kapalı issues.num_comments=%d yorum -issues.commented_at=`%s olarak yorumlandı` +issues.commented_at=`%s yorum yaptı` issues.delete_comment_confirm=Bu yorumu silmek istediğinizden emin misiniz? issues.no_content=Henüz bir içerik yok. issues.close_issue=Kapat @@ -784,11 +810,11 @@ issues.reopen_comment_issue=Yorum Yap ve Yeniden Aç issues.create_comment=Yorum yap issues.closed_at=`%[2]s kapattı` issues.reopened_at=`%[2]s yeniden açtı` -issues.commit_ref_at=`%[2]s işlemesinde bu sorunu işaret etti` -issues.ref_issue_at=`bu konudan bahsetti %[1]s` -issues.ref_pull_at=`bu değişiklik isteğinden bahsetti %[1]s` -issues.ref_issue_ext_at=`%[1]s'den bu konuya değinildi %[2]s` -issues.ref_pull_ext_at=`%[1]s'den bu değişiklik isteğine değinildi %[2]s` +issues.commit_ref_at=`%[2]s işlemesinde bu konuyu işaret etti` +issues.ref_issue_at=`%[1]s bu konudan bahsetti` +issues.ref_pull_at=`%[1]s bu değişiklik isteğinden bahsetti` +issues.ref_issue_ext_at=`%[2]s %[1]s'den bu konuya değinildi` +issues.ref_pull_ext_at=`%[2]s %[1]s'den bu değişiklik isteğine değinildi` issues.poster=Poster issues.collaborator=Katkıcı issues.owner=Sahibi @@ -800,7 +826,7 @@ issues.label_title=Etiket adı issues.label_description=Etiket açıklaması issues.label_color=Etiket rengi issues.label_count=%d etiket -issues.label_open_issues=%d açık sorun +issues.label_open_issues=%d açık konu issues.label_edit=Düzenle issues.label_delete=Sil issues.label_modify=Etiketi Düzenle @@ -835,11 +861,15 @@ issues.lock.reason=Kilitleme nedeni issues.lock.title=Konuşmayı kilitle. issues.unlock.title=Konuşmanın kilidini aç. issues.comment_on_locked=Kilitli bir konuya yorum yapamazsınız. +issues.tracker=Zaman İzleyici issues.start_tracking_short=Başlat +issues.start_tracking=Zaman İzlemeyi Başlat issues.start_tracking_history=`%s çalışması başlatıldı` +issues.tracker_auto_close=Bu konu kapatıldığında zamanlayıcı otomatik olarak durur issues.tracking_already_started=`Bu konuda zaten zaman izleyicisini başlattınız!` issues.stop_tracking=Durdur issues.stop_tracking_history=`%s çalışması durduruldu` +issues.add_time=El ile Zaman Ekle issues.add_time_short=Zaman Ekle issues.add_time_cancel=İptal issues.add_time_history=`%s harcanan zaman eklendi` @@ -860,6 +890,10 @@ issues.due_date_form_edit=Düzenle issues.due_date_form_remove=Kaldır issues.due_date_not_writer=Bir konunun bitiş tarihini değiştirmek için depoda yazma hakkınız olmalıdır. issues.due_date_not_set=Bitiş tarihi atanmadı. +issues.due_date_added=%[2]s %[1]s bitiş tarihini ekledi +issues.due_date_modified=%[3]s bitiş tarihi %[2]s iken %[1]s olarak değiştirildi +issues.due_date_remove=%[2]s %[1]s bitiş tarihini kaldırdı +issues.due_date_invalid=Bitiş tarihi geçersiz veya aralık dışında. Lütfen 'yyyy-aa-gg' biçimini kullanın. issues.dependency.title=Bağımlılıklar issues.dependency.issue_no_dependencies=Bu konu henüz bir bağımlılık içermiyor. issues.dependency.pr_no_dependencies=Bu çekme isteği henüz bir bağımlılık içermiyor. @@ -867,29 +901,32 @@ issues.dependency.add=Bağımlılık ekle… issues.dependency.cancel=İptal issues.dependency.remove=Kaldır issues.dependency.remove_info=Bu bağımlılığı kaldır -issues.dependency.added_dependency=`%[2]s yeni bir bağımlık ekledi %[3]s` -issues.dependency.removed_dependency=`%[2]s bir bağımlılığı kaldırdı %[3]s` -issues.dependency.issue_closing_blockedby=Bu değişiklik isteğinin kapatılması aşağıdaki sorunlar nedeniyle engelleniyor -issues.dependency.pr_closing_blockedby=Bu sorunun kapatılması aşağıdaki sorunlar nedeniyle engelleniyor -issues.dependency.issue_close_blocks=Bu sorun aşağıdaki sorunların kapatılmasını engelliyor +issues.dependency.added_dependency=`%[2]s %[3]s yeni bir bağımlık ekledi` +issues.dependency.removed_dependency=`%[2]s %[3]s bir bağımlılığı kaldırdı` +issues.dependency.issue_closing_blockedby=Bu değişiklik isteğinin kapatılması aşağıdaki konular nedeniyle engelleniyor +issues.dependency.pr_closing_blockedby=Bu konunun kapatılması aşağıdaki konular tarafından engelleniyor +issues.dependency.issue_close_blocks=Bu konu aşağıdaki konuların kapatılmasını engelliyor issues.dependency.pr_close_blocks=Bu değişiklik isteği aşağıdaki sorunların kapatılmasını engelliyor -issues.dependency.issue_close_blocked=Kapatmadan önce bu sorunu engelleyen tüm sorunları kapatmanız gerekir. -issues.dependency.pr_close_blocked=Birleştirme işleminden önce, bu değişiklik isteğini engelleyen tüm sorunları kapatmanız gerekir. +issues.dependency.issue_close_blocked=Kapatmadan önce bu konuyu engelleyen tüm konuları kapatmanız gerekir. +issues.dependency.pr_close_blocked=Birleştirme işleminden önce, bu değişiklik isteğini engelleyen tüm konuları kapatmanız gerekir. issues.dependency.blocks_short=Engeller issues.dependency.blocked_by_short=Bağımlılıklar issues.dependency.remove_header=Bağımlılığı Kaldır issues.dependency.issue_remove_text=Bu işlem, bu konudaki bağımlılığı kaldıracaktır. Devam edilsin mi? issues.dependency.pr_remove_text=Bu işlem, bu değişiklik isteğindeki bağımlılığı kaldıracaktır. Devam edilsin mi? issues.dependency.setting=Konular ve Değişiklik İstekleri İçin Bağımlılıkları Etkinleştir -issues.dependency.add_error_same_issue=Bir sorunu kendine bağımlı yapamazsınız. -issues.dependency.add_error_dep_issue_not_exist=Bağımlı sorun mevcut değil. +issues.dependency.add_error_same_issue=Bir konuyu kendine bağımlı yapamazsınız. +issues.dependency.add_error_dep_issue_not_exist=Bağımlı konu mevcut değil. issues.dependency.add_error_dep_not_exist=Bağımlılık mevcut değil. issues.dependency.add_error_dep_exists=Bağımlılık zaten var. -issues.dependency.add_error_cannot_create_circular=Birbirini engelleyen iki sorunla bir bağımlılık oluşturamazsınız. +issues.dependency.add_error_cannot_create_circular=Birbirini engelleyen iki konu arasında bağımlılık oluşturamazsınız. issues.dependency.add_error_dep_not_same_repo=Her iki konu da aynı depoda olmalıdır. issues.review.self.approval=Kendi değişiklik isteğinizi onaylayamazsınız. issues.review.self.rejection=Kendi değişiklik isteğinizde değişiklik isteyemezsiniz. +issues.review.approve=%s bu değişiklikleri onayladı +issues.review.comment=%s incelendi issues.review.content.empty=İstenen değişiklik(ler)i belirten bir yorum bırakmanız gerekir. +issues.review.reject=%s değişiklik istedi issues.review.pending=Beklemede issues.review.review=Gözden Geçir issues.review.reviewers=Gözden Geçirenler @@ -897,6 +934,7 @@ issues.review.reviewers=Gözden Geçirenler pulls.desc=Değişiklik isteklerini ve kod incelemelerini etkinleştir. pulls.new=Yeni Değişiklik İsteği pulls.compare_changes=Yeni Değişiklik İsteği +pulls.compare_changes_desc=Birleştirmek için hedef ve kaynak dalı seçin. pulls.compare_compare=şuradan çek pulls.filter_branch=Dal filtrele pulls.no_results=Sonuç bulunamadı. @@ -904,7 +942,7 @@ pulls.nothing_to_compare=Bu dallar eşit. Değişiklik isteği oluşturmaya gere pulls.has_pull_request=`Bu dallar arasında bir değişiklik isteği zaten var: %[2]s#%[3]d` pulls.create=Değişiklik İsteği Oluştur pulls.title_desc=%[3]s içindeki %[2]s işlemelerini %[1]d ile birleştirmek istiyor -pulls.merged_title_desc=%[3]s %[4]s içindeki %[2]s işlemelerini %[1]d ile birleştirdi +pulls.merged_title_desc=%[4]s %[2]s içindeki %[1]d işleme %[3]s ile birleştirdi pulls.tab_conversation=Sohbet pulls.tab_commits=İşlemeler pulls.tab_files=Değiştirilen Dosyalar @@ -966,8 +1004,8 @@ milestones.filter_sort.closest_due_date=En yakın zamanı gelmiş tarih milestones.filter_sort.furthest_due_date=En uzak zamanı gelmiş tarih milestones.filter_sort.least_complete=En az tamamlama milestones.filter_sort.most_complete=En çok tamamlama -milestones.filter_sort.most_issues=En çok sorun -milestones.filter_sort.least_issues=En az sorun +milestones.filter_sort.most_issues=En çok konu +milestones.filter_sort.least_issues=En az konu ext_wiki=Harici Wiki ext_wiki.desc=Harici bir wiki'ye bağlantı. @@ -985,8 +1023,11 @@ wiki.save_page=Sayfayı Kaydet wiki.last_commit_info=%s bu sayfayı düzenledi %s wiki.edit_page_button=Düzenle wiki.new_page_button=Yeni Sayfa +wiki.file_revision=Sayfa Revizyonu +wiki.wiki_page_revisions=Wiki Sayfası Revizyonları wiki.back_to_wiki=Wiki sayfasına geri dön wiki.delete_page_button=Sayfayı Sil +wiki.delete_page_notice_1='%s' wiki sayfasını silmek geri alınamaz. Devam edilsin mi? wiki.page_already_exists=Aynı isimde bir Wiki sayfası zaten var. wiki.reserved_page='%s' wiki sayfa adı rezerve edilmiştir. wiki.pages=Sayfalar @@ -1113,10 +1154,13 @@ settings.transfer_notices_1=- Bireysel bir kullanıcıya aktarırsanız depoya e settings.transfer_form_title=Onaylamak için depo adını girin: settings.wiki_delete=Wiki Verisini Sil settings.wiki_delete_desc=Depo wiki verilerini silmek kalıcıdır ve geri alınamaz. +settings.wiki_delete_notices_1=- Bu işlem, %s için depo wiki'sini kalıcı olarak siler ve devre dışı bırakır. settings.confirm_wiki_delete=Wiki Verisini Sil settings.wiki_deletion_success=Depo wiki verisi silindi. settings.delete=Bu Depoyu Sil +settings.delete_desc=Bir depoyu silmek kalıcıdır ve geri alınamaz. settings.delete_notices_1=- Bu işlem geri ALINAMAZ. +settings.delete_notices_fork_1=- Silme işleminden sonra bu deponun çatalları bağımsız hale gelecektir. settings.deletion_success=Depo silindi. settings.update_settings_success=Depo ayarları güncellendi. settings.transfer_owner=Yeni Sahip @@ -1137,7 +1181,9 @@ settings.change_team_access_not_allowed=Depo için takım erişimini değiştirm settings.team_not_in_organization=Takım, depo ile aynı organizasyonda değil settings.add_team_duplicate=Takım zaten bu depoya sahip settings.add_team_success=Takım artık bu depoya erişebilir. +settings.remove_team_success=Takımın depoya erişimi kaldırıldı. settings.add_webhook=Web İsteği Ekle +settings.add_webhook.invalid_channel_name=Web istemci kanal adı boş olamaz ve yalnızca bir # karakteri içeremez. settings.hooks_desc=Web istemcileri, belirli Gitea olayları tetiklendiğinde otomatik olarak bir sunucuya HTTP POST isteği yapar. Web istekleri kılavuzundan daha fazla bilgi edinebilirsiniz. settings.webhook_deletion=Web İsteğini Sil settings.webhook_deletion_desc=Bir web isteğini kaldırmak, ayarlarını ve teslimat geçmişini siler. Devam edilsin mi? @@ -1274,7 +1320,9 @@ diff.parent=ebeveyn diff.commit=işleme diff.git-notes=Notlar diff.data_not_available=Farklı İçerik Mevut Değil -diff.show_diff_stats=Farklılık Durumunu Göster +diff.options_button=Diff Seçenekleri +diff.download_patch=Yama Dosyasını İndir +diff.download_diff=Diff Dosyasını İndir diff.show_split_view=Görünümü Böl diff.show_unified_view=Birleşik Görünüm diff.whitespace_button=Boşluk @@ -1299,10 +1347,13 @@ diff.comment.add_review_comment=Yorum ekle diff.comment.start_review=İncelemeye başla diff.comment.reply=Yanıtla diff.review=İncele +diff.review.header=İnceleme gönder +diff.review.placeholder=İnceleme yorumu diff.review.comment=Yorum Yap diff.review.approve=Onayla diff.review.reject=Değişiklik iste +releases.desc=Proje sürümlerini ve indirmeleri takip edin. release.releases=Sürümler release.new_release=Yeni Sürüm release.draft=Taslak @@ -1354,6 +1405,7 @@ branch.restore_failed='%s' dalı geri yüklenemedi. branch.protected_deletion_failed='%s' dalı korunuyor. Silinemez. branch.restore='%s' Dalını Geri Yükle branch.download='%s' Dalını İndir +branch.included_desc=Bu dal varsayılan dalın bir parçasıdır topic.done=Bitti @@ -1560,7 +1612,7 @@ repos.private=Özel repos.watches=İzlemeler repos.stars=Yıldızlar repos.forks=Çatallar -repos.issues=Sorunlar +repos.issues=Konular repos.size=Boyut hooks.add_webhook=Varsayılan Web İstemcisi Ekle @@ -1623,7 +1675,14 @@ auths.tip.gitea=Yeni bir OAuth2 uygulaması kaydedin. Rehber https://docs.gitea. auths.edit=Kimlik Doğrulama Kaynağı Düzenle auths.activated=Bu Kimlik Doğrulama Kaynağı Etkinleştirildi auths.new_success=Kimlik doğrulama '%s' eklendi. +auths.update_success=Kimlik doğrulama kaynağı güncellendi. +auths.update=Kimlik Doğrulama Kaynağını Güncelle +auths.delete=Kimlik Doğrulama Kaynağını Sil auths.delete_auth_title=Kimlik Doğrulama Kaynağını Sil +auths.delete_auth_desc=Bir kimlik doğrulama kaynağını silmek, kullanıcıların oturum açmak için kullanmalarını engeller. Devam edilsin mi? +auths.still_in_used=Kimlik doğrulama kaynağı hala kullanımda. Önce bu kimlik doğrulama kaynağını kullanan tüm kullanıcıları dönüştürün veya silin. +auths.deletion_success=Kimlik doğrulama kaynağı silindi. +auths.login_source_exist='%s' kimlik doğrulama kaynağı zaten var. config.server_config=Sunucu Yapılandırması config.app_name=Site Başlığı @@ -1633,7 +1692,7 @@ config.offline_mode=Yerel Kip config.disable_router_log=Yönlendirici Log'larını Devre Dışı Bırak config.run_user=Şu Kullanıcı Olarak Çalıştır config.run_mode=Çalıştırma Modu -config.git_version=Git Sürüm +config.git_version=Git Sürümü config.repo_root_path=Depo Kök Yolu config.lfs_root_path=LFS Kök Dizini config.static_file_root_path=Sabit Dosya Kök Yolu @@ -1675,17 +1734,22 @@ config.disable_key_size_check=Minimum Anahtar Uzunluğu Kontrolünü Devre Dış config.enable_captcha=CAPTCHA'yı Etkinleştir config.active_code_lives=Kod Yaşamlarını Aktifleştir config.default_keep_email_private=E-posta Adreslerini Varsayılan Olarak Gizle +config.default_visibility_organization=Yeni organizasyonlar için varsayılan görünürlük +config.default_enable_dependencies=Konu Bağımlılıklarını Varsayılan Olarak Etkinleştir config.webhook_config=Web İstekleri Yapılandırması config.queue_length=Kuyruk Uzunluğu config.deliver_timeout=Dağıtım Zaman Aşımı config.skip_tls_verify=TLS Doğrulamasını Geç +config.mailer_config=SMTP Mailer Yapılandırması config.mailer_enabled=Aktif config.mailer_disable_helo=HELO'yu Devre Dışı Bırak config.mailer_name=İsim config.mailer_host=Sunucu config.mailer_user=Kullanıcı +config.mailer_use_sendmail=Sendmail Kullan +config.mailer_sendmail_path=Sendmail Yolu config.send_test_mail=Test E-postası Gönder config.test_mail_failed='%s' adresine test e-postası gönderilemedi: %v config.test_mail_sent='%s' adresine bir test e-postası gönderildi. @@ -1708,6 +1772,7 @@ config.session_life_time=Oturum Yaşam Zamanı config.https_only=Yalnız HTTPS config.cookie_life_time=Çerez Yaşam Zamanı +config.picture_config=Resim ve Avatar Yapılandırması config.picture_service=Resim Servisi config.disable_gravatar=Gravatar Hizmet Dışı @@ -1727,6 +1792,8 @@ config.log_config=Log Yapılandırması config.log_mode=Log Modu config.macaron_log_mode=Macaron Günlük Kipi config.own_named_logger=Adlandırılmış Günlük Kaydedicisi +config.routes_to_default_logger=Varsayılan Günlük Kaydedicisine Yönlendirir +config.go_log=Go Log kullanılır (varsayılana yönlendirilir) config.router_log_mode=Yönlendirici Günlük Kipi config.disabled_logger=Devre Dışı config.access_log_mode=Erişim Günlüğü Kipi @@ -1763,13 +1830,13 @@ notices.delete_success=Sistem bildirimleri silindi. create_repo=depo %s oluşturuldu rename_repo=%[1]s olan depo adını %[3]s buna çevirdi commit_repo=%[4]s deposunda %[3]s dalını itti -create_issue=`%s#%[2]s sorununu açtı` -close_issue=`%s#%[2]s sorununu kapattı` -reopen_issue=`%s#%[2]s sorununu tekrar açtı` +create_issue=`%s#%[2]s konusunu açtı` +close_issue=`%s#%[2]s konusunu kapattı` +reopen_issue=`%s#%[2]s konusunu tekrar açtı` create_pull_request=`%s#%[2]s değişiklik isteğini oluşturdu` close_pull_request=`%s#%[2]s değişiklik isteğini kapattı` reopen_pull_request=`%s#%[2]s değişiklik isteğini tekrar açtı` -comment_issue=`%s#%[2]s sorununa yorum yazdı` +comment_issue=`%s#%[2]s konusuna yorum yazdı` merge_pull_request=`%s#%[2]s değişim isteğini birleştirdi` transfer_repo=depo %s %s'a aktarıldı push_tag=%[3]s deposuna %[2]s etiketi itildi @@ -1778,6 +1845,7 @@ delete_branch=%[3]s deposundan %[2]s dalı silindi compare_commits=%d işlemeyi karşılaştır compare_commits_general=İşlemeleri karşılaştır mirror_sync_push=işlemeler yansıdan %[4]s deposundaki %[3]s dalına eşitlendi +mirror_sync_delete=%[3]s adresindeki %[2]s referansı eşitlendi ve silindi [tool] ago=%s önce @@ -1819,12 +1887,15 @@ mark_as_unread=Okunmadı olarak işaretle mark_all_as_read=Tümünü okundu olarak işaretle [gpg] +default_key=Varsayılan anahtarla imzalanmış error.extract_sign=İmza çıkarılamadı error.generate_hash=İşlemenin sağlama kodu oluşturulamadı error.no_committer_account=İşleme yapanın e-posta adresine bağlı hesap yok error.no_gpg_keys_found=Veri tabanında bu imza için bilinen anahtar bulunamadı error.not_signed_commit=İmzalı bir işleme değil error.failed_retrieval_gpg_keys=İşleme yapanın hesabına bağlı herhangi bir anahtar alınamadı +error.probable_bad_signature=UYARI! Veritabanında bu kimliğe sahip bir anahtar olmasına rağmen, bu işlemeyi doğrulamaz! Bu işleme ŞÜPHELİDİR. +error.probable_bad_default_signature=UYARI! Varsayılan anahtarın bu kimliği olmasına rağmen, bu işlemeyi doğrulamaz! Bu işleme ŞÜPHELİDİR. [units] error.no_unit_allowed_repo=Bu deponun hiçbir bölümüne erişme izniniz yok. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 22d30705db..51e52b2785 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1,5 +1,3 @@ -app_desc=Зручний сервіс, власного Git хостингу - home=Головна dashboard=Панель управління explore=Огляд @@ -75,6 +73,8 @@ write=Писати preview=Попередній перегляд loading=Завантаження… +[startpage] + [install] install=Встановлення title=Початкова конфігурація @@ -1136,7 +1136,6 @@ diff.browse_source=Переглянути джерело diff.parent=джерело diff.commit=коміт diff.data_not_available=Різниця недоступна -diff.show_diff_stats=Показати статистику Diff diff.show_split_view=Розділений перегляд diff.show_unified_view=Об'єднаний перегляд diff.whitespace_button=Пробіли diff --git a/options/locale/locale_vi-VN.ini b/options/locale/locale_vi-VN.ini index f2571ce9c0..35af054c15 100644 --- a/options/locale/locale_vi-VN.ini +++ b/options/locale/locale_vi-VN.ini @@ -6,6 +6,7 @@ +[startpage] [install] diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 7667609793..6a0ebed2be 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1,5 +1,3 @@ -app_desc=一款极易搭建的自助 Git 服务 - home=首页 dashboard=首页 explore=探索 @@ -75,6 +73,17 @@ write=撰写 preview=预览 loading=正在加载... +[startpage] +app_desc=一款极易搭建的自助 Git 服务 +install=易安装 +install_desc=您除了可以根据操作系统平台通过 二进制运行,还可以通过 DockerVagrant,以及 包管理 安装。 +platform=跨平台 +platform_desc=任何 Go 语言 支持的平台都可以运行 Gitea,包括 Windows、Mac、Linux 以及 ARM。挑一个您喜欢的就行! +lightweight=轻量级 +lightweight_desc=一个廉价的树莓派的配置足以满足 Gitea 的最低系统硬件要求。最大程度上节省您的服务器资源! +license=开源化 +license_desc=所有的代码都开源在 GitHub 上,赶快加入我们来共同发展这个伟大的项目!还等什么?成为贡献者吧! + [install] install=安装页面 title=初始配置 @@ -250,7 +259,6 @@ openid_signin_desc=输入您的 OpenID URI。例如: https://anne.me、bob.openi disable_forgot_password_mail=帐户恢复功能已被禁用。请与网站管理员联系。 email_domain_blacklisted=您不能使用您的电子邮件地址注册。 authorize_application=应用授权 -authroize_redirect_notice=如果您授权此应用,您将会被重定向到 %s。 authorize_application_created_by=此应用由%s创建。 authorize_application_description=如果您允许,它将能够读取和修改您的所有帐户信息,包括私人仓库和组织。 authorize_title=授权 %s 访问您的帐户? @@ -314,6 +322,7 @@ team_no_units_error=至少选择一项仓库单元。 email_been_used=该电子邮件地址已在使用中。 openid_been_used=OpenID 地址 '%s' 已被使用。 username_password_incorrect=用户名或密码不正确。 +password_complexity=密码未达到复杂程度要求 enterred_invalid_repo_name=输入的仓库名称不正确 enterred_invalid_owner_name=新的所有者名称无效。 enterred_invalid_password=输入的密码不正确 @@ -632,6 +641,8 @@ migrate.lfs_mirror_unsupported=不支持镜像 LFS 对象 - 使用 'git lfs fetc migrate.migrate_items_options=当从 github 迁移并且输入了用户名时,迁移选项将会显示。 migrated_from=从 %[2]s 迁移 migrated_from_fake=从 %[1]s 迁移成功 +migrate.migrating=正在从 %s 迁移... +migrate.migrating_failed=从 %s 迁移失败。 mirror_from=镜像自地址 forked_from=派生自 @@ -680,6 +691,8 @@ stored_lfs=存储到Git LFS commit_graph=提交图 blame=Blame normal_view=普通视图 +line=行 +lines=行 editor.new_file=新建文件 editor.upload_file=上传文件 @@ -705,6 +718,7 @@ editor.delete=删除 '%s' editor.commit_message_desc=添加一个可选的扩展描述... editor.commit_directly_to_this_branch=直接提交至 %s 分支。 editor.create_new_branch=为此提交创建一个 新的分支 并发起合并请求。 +editor.create_new_branch_np=为此提交创建 新分支。 editor.propose_file_change=提议文件更改 editor.new_branch_name_desc=新的分支名称... editor.cancel=取消 @@ -719,6 +733,8 @@ editor.file_editing_no_longer_exists=正在编辑的文件 '%s' 已不存在。 editor.file_deleting_no_longer_exists=仓库中不存在将被删除的文件‘%s’。 editor.file_changed_while_editing=文件内容在您进行编辑时已经发生变动。单击此处 查看变动的具体内容,或者 再次提交 覆盖已发生的变动。 editor.file_already_exists=此仓库已经存在名为 '%s' 的文件。 +editor.commit_empty_file_header=提交一个空文件 +editor.commit_empty_file_text=您要提交的文件是空的,继续吗? editor.no_changes_to_show=没有可以显示的变更。 editor.fail_to_update_file=更新/创建文件 '%s' 时发生错误:%v editor.add_subdir=添加目录 @@ -1363,7 +1379,10 @@ diff.parent=父节点 diff.commit=当前提交 diff.git-notes=Notes diff.data_not_available=比较内容不可用 -diff.show_diff_stats=显示文件统计 +diff.options_button=Diff 选项 +diff.show_diff_stats=显示统计 +diff.download_patch=下载 Patch 文件 +diff.download_diff=下载 Diff 文件 diff.show_split_view=分列视图 diff.show_unified_view=合并视图 diff.whitespace_button=空白符号 @@ -1446,6 +1465,8 @@ branch.restore_failed=未能还原分支%s。 branch.protected_deletion_failed=分支 '%s' 已被保护,不可删除。 branch.restore=恢复分支 '%s' branch.download=下载分支 '%s' +branch.included_desc=此分支是默认分支的一部分 +branch.included=已包含 topic.manage_topics=管理主题 topic.done=保存 @@ -1962,12 +1983,15 @@ mark_as_unread=标记为未读 mark_all_as_read=全部标记为已读 [gpg] +default_key=使用默认密钥签名 error.extract_sign=无法提取签名 error.generate_hash=无法生成提交的哈希 error.no_committer_account=没有帐户链接到提交者的电子邮件 error.no_gpg_keys_found=找不到此签名对应的密钥 error.not_signed_commit=未签名的提交 error.failed_retrieval_gpg_keys=找不到任何与该提交者账号相关的密钥 +error.probable_bad_signature=警告!虽然数据库中有一个此ID的密钥,但它没有验证此提交!此提交是有疑问的。 +error.probable_bad_default_signature=警告!虽然默认密钥拥有此ID,但它没有验证此提交!此提交是有疑问的。 [units] error.no_unit_allowed_repo=您没有被允许访问此仓库的任何单元。 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index ab97fccb6b..43e9b3eea5 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -1,4 +1,3 @@ - home=首頁 dashboard=控制面版 explore=探索 @@ -43,6 +42,8 @@ issues=問題 cancel=取消 +[startpage] + [install] install=安裝頁面 db_title=資料庫設定 @@ -513,7 +514,6 @@ diff.browse_source=瀏覽代碼 diff.parent=父節點 diff.commit=當前提交 diff.data_not_available=沒有內容比較可以使用 -diff.show_diff_stats=顯示文件統計 diff.show_split_view=分割檢視 diff.show_unified_view=統一視圖 diff.stats_desc=共有 %d 個文件被更改,包括 %d 次插入%d 次删除 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 796906312b..de787b6b47 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1,5 +1,3 @@ -app_desc=一個無痛、自託管的 Git 服務 - home=首頁 dashboard=控制面版 explore=探索 @@ -64,6 +62,8 @@ issues=問題 cancel=取消 +[startpage] + [install] install=安裝頁面 title=初始設定 @@ -919,7 +919,6 @@ diff.browse_source=瀏覽代碼 diff.parent=父節點 diff.commit=當前提交 diff.data_not_available=沒有內容比較可以使用 -diff.show_diff_stats=顯示文件統計 diff.show_split_view=分割檢視 diff.show_unified_view=統一視圖 diff.stats_desc=共有 %d 個檔案被更改,包括 %d 行新增%d 行删除 diff --git a/public/css/index.css b/public/css/index.css index 496194decc..e404c1fec6 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -225,6 +225,10 @@ footer .ui.left,footer .ui.right{line-height:40px} .inline-grouped-list{display:inline-block;vertical-align:top} .inline-grouped-list>.ui{display:block;margin-top:5px;margin-bottom:10px} .inline-grouped-list>.ui:first-child{margin-top:1px} +i.icons .icon:first-child{margin-right:0} +i.icon.centerlock{top:1.5em} +.ui.label>.detail .icons{margin-right:.25em} +.ui.label>.detail .icons .icon{margin-right:0} .lines-num{vertical-align:top;text-align:right!important;color:#999;background:#f5f5f5;width:1%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} .lines-num span:before{content:attr(data-line-number);line-height:20px!important;padding:0 10px;cursor:pointer;display:block} .lines-code,.lines-num{padding:0!important} @@ -239,7 +243,7 @@ footer .ui.left,footer .ui.right{line-height:40px} .lines-commit .ui.avatar.image{height:18px;width:18px} .lines-code .bottom-line,.lines-commit .bottom-line,.lines-num .bottom-line{border-bottom:1px solid #eaecef} .code-view{overflow:auto;overflow-x:auto;overflow-y:hidden} -.code-view *{font-size:12px;font-family:'SF Mono',Consolas,Menlo,'Liberation Mono',Monaco,'Lucida Console',monospace;line-height:20px} +.code-view :not(.fa):not(.octicon):not(.icon){font-size:12px;font-family:'SF Mono',Consolas,Menlo,'Liberation Mono',Monaco,'Lucida Console',monospace;line-height:20px} .code-view table{width:100%} .code-view .active{background:#fff866} .markdown:not(code){overflow:hidden;font-size:16px;line-height:1.6!important;word-wrap:break-word} @@ -392,6 +396,8 @@ footer .ui.left,footer .ui.right{line-height:40px} .user.activate form .inline.field>label,.user.forgot.password form .inline.field>label,.user.reset.password form .inline.field>label,.user.signin form .inline.field>label,.user.signup form .inline.field>label{width:200px} @media only screen and (max-width:768px){.user.activate form .inline.field>label,.user.activate form input,.user.forgot.password form .inline.field>label,.user.forgot.password form input,.user.reset.password form .inline.field>label,.user.reset.password form input,.user.signin form .inline.field>label,.user.signin form input,.user.signup form .inline.field>label,.user.signup form input{width:100%!important} } +.user.activate form input[type=number],.user.forgot.password form input[type=number],.user.reset.password form input[type=number],.user.signin form input[type=number],.user.signup form input[type=number]{-moz-appearance:textfield} +.user.activate form input::-webkit-inner-spin-button,.user.activate form input::-webkit-outer-spin-button,.user.forgot.password form input::-webkit-inner-spin-button,.user.forgot.password form input::-webkit-outer-spin-button,.user.reset.password form input::-webkit-inner-spin-button,.user.reset.password form input::-webkit-outer-spin-button,.user.signin form input::-webkit-inner-spin-button,.user.signin form input::-webkit-outer-spin-button,.user.signup form input::-webkit-inner-spin-button,.user.signup form input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} .repository.new.fork form,.repository.new.migrate form,.repository.new.repo form{margin:auto} .repository.new.fork form .ui.message,.repository.new.migrate form .ui.message,.repository.new.repo form .ui.message{text-align:center} @media only screen and (min-width:768px){.repository.new.fork form,.repository.new.migrate form,.repository.new.repo form{width:800px!important} @@ -452,6 +458,8 @@ footer .ui.left,footer .ui.right{line-height:40px} .repository .filter.menu .label.color{border-radius:3px;margin-left:15px;padding:0 8px} .repository .filter.menu .octicon{float:left;margin:5px -7px 0 -5px;width:16px} .repository .filter.menu.labels .octicon{margin:-2px -7px 0 -5px} +.repository .filter.menu.labels .label-filter .menu .info{display:inline-block;padding:9px 7px 7px 7px;text-align:center;border-bottom:1px solid #ccc;font-size:12px} +.repository .filter.menu.labels .label-filter .menu .info code{border:1px solid #ccc;border-radius:3px;padding:3px 2px 1px 2px;font-size:11px} .repository .filter.menu .text{margin-left:.9em} .repository .filter.menu .menu{max-height:300px;overflow-x:auto;right:0!important;left:auto!important} .repository .filter.menu .dropdown.item{margin:1px;padding-right:0} @@ -494,7 +502,7 @@ footer .ui.left,footer .ui.right{line-height:40px} .repository.file.list #repo-files-table tr:hover{background-color:#ffe} .repository.file.list #repo-files-table .jumpable-path{color:#888} .repository.file.list .non-diff-file-content .header .icon{font-size:1em} -.repository.file.list .non-diff-file-content .header .file-actions{margin-top:0;margin-bottom:-5px;padding-left:20px} +.repository.file.list .non-diff-file-content .header .file-actions{margin-bottom:-5px} .repository.file.list .non-diff-file-content .header .file-actions .btn-octicon{display:inline-block;padding:5px;margin-left:5px;line-height:1;color:#767676;vertical-align:middle;background:0 0;border:0;outline:0} .repository.file.list .non-diff-file-content .header .file-actions .btn-octicon:hover{color:#4078c0} .repository.file.list .non-diff-file-content .header .file-actions .btn-octicon-danger:hover{color:#bd2c00} @@ -654,13 +662,13 @@ footer .ui.left,footer .ui.right{line-height:40px} .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n){background-color:rgba(0,0,0,.02)!important} .repository #commits-table td.sha .sha.label,.repository #repo-files-table .sha.label{border:1px solid #bbb} .repository #commits-table td.sha .sha.label .detail.icon,.repository #repo-files-table .sha.label .detail.icon{background:#fafafa;margin:-6px -10px -4px 0;padding:5px 3px 5px 6px;border-left:1px solid #bbb;border-top-left-radius:0;border-bottom-left-radius:0} +.repository #commits-table td.sha .sha.label.isSigned.isWarning,.repository #repo-files-table .sha.label.isSigned.isWarning{border:1px solid #db2828;background:rgba(219,40,40,.1)} +.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon,.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon{border-left:1px solid rgba(219,40,40,.5)} .repository #commits-table td.sha .sha.label.isSigned.isVerified,.repository #repo-files-table .sha.label.isSigned.isVerified{border:1px solid #21ba45;background:rgba(33,186,69,.1)} .repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon,.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon{border-left:1px solid #21ba45} .repository #commits-table td.sha .sha.label.isSigned.isVerified:hover,.repository #repo-files-table .sha.label.isSigned.isVerified:hover{background:rgba(33,186,69,.3)!important} .repository .diff-detail-box{padding:7px 0;background:#fff;line-height:30px} .repository .diff-detail-box>div:after{clear:both;content:"";display:block} -.repository .diff-detail-box ol{clear:both;padding-left:0;margin-top:5px;margin-bottom:28px} -.repository .diff-detail-box ol li{list-style:none;padding-bottom:4px;margin-bottom:4px;border-bottom:1px dashed #ddd;padding-left:6px} .repository .diff-detail-box span.status{display:inline-block;width:12px;height:12px;margin-right:8px;vertical-align:middle} .repository .diff-detail-box span.status.modify{background-color:#f0db88} .repository .diff-detail-box span.status.add{background-color:#b4e2b4} @@ -697,6 +705,11 @@ footer .ui.left,footer .ui.right{line-height:40px} .repository .diff-file-box.file-content{clear:right} .repository .diff-file-box.file-content img{max-width:100%;padding:5px 5px 0 5px} .repository .diff-file-box.file-content img.emoji{padding:0} +.repository .diff-stats{clear:both;margin-bottom:5px;max-height:400px;overflow:auto;padding-left:0} +.repository .diff-stats li{list-style:none;padding-bottom:4px;margin-bottom:4px;border-bottom:1px dashed #ddd;padding-left:6px} +.repository .diff-stats .diff-counter{margin-right:15px} +.repository .diff-stats .diff-counter .del{color:red} +.repository .diff-stats .diff-counter .add{color:green} .repository .repo-search-result{padding-top:10px;padding-bottom:10px} .repository .repo-search-result .lines-num a{color:inherit} .repository.quickstart .guide .item{padding:1em} @@ -878,6 +891,10 @@ tbody.commit-list{vertical-align:baseline} .repo-buttons .disabled-repo-button a.button:hover{background:0 0!important;color:rgba(0,0,0,.6)!important;box-shadow:0 0 0 1px rgba(34,36,38,.15) inset!important} .repo-buttons .ui.labeled.button>.label{border-left:0!important;margin:0!important} .tag-code,.tag-code td{background-color:#f0f0f0!important;border-color:#d3cfcf!important;padding-top:8px;padding-bottom:8px} +.issue-keyword{border-bottom:1px dotted #959da5;display:inline-block} +.file-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px!important} +.file-info{display:flex;align-items:center} +.file-info-entry+.file-info-entry{border-left:1px solid currentColor;margin-left:8px;padding-left:8px} .CodeMirror{font:14px 'SF Mono',Consolas,Menlo,'Liberation Mono',Monaco,'Lucida Console',monospace} .CodeMirror.cm-s-default{border-radius:3px;padding:0!important} .CodeMirror .cm-comment{background:inherit!important} diff --git a/public/img/loading.png b/public/img/loading.png new file mode 100644 index 0000000000..aac702cfd6 Binary files /dev/null and b/public/img/loading.png differ diff --git a/public/js/index.js b/public/js/index.js index 8a85ad9157..e76e993a1d 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -241,6 +241,41 @@ function updateIssuesMeta(url, action, issueIds, elementId) { }) } +function initRepoStatusChecker() { + const migrating = $("#repo_migrating"); + $('#repo_migrating_failed').hide(); + if (migrating) { + const repo_name = migrating.attr('repo'); + if (typeof repo_name === 'undefined') { + return + } + $.ajax({ + type: "GET", + url: suburl +"/"+repo_name+"/status", + data: { + "_csrf": csrf, + }, + complete: function(xhr) { + if (xhr.status == 200) { + if (xhr.responseJSON) { + if (xhr.responseJSON["status"] == 0) { + location.reload(); + return + } + + setTimeout(function () { + initRepoStatusChecker() + }, 2000); + return + } + } + $('#repo_migrating_progress').hide(); + $('#repo_migrating_failed').show(); + } + }) + } +} + function initReactionSelector(parent) { let reactions = ''; if (!parent) { @@ -271,7 +306,7 @@ function initReactionSelector(parent) { if (resp && (resp.html || resp.empty)) { const content = $(vm).closest('.content'); let react = content.find('.segment.reactions'); - if (react.length > 0) { + if (!resp.empty && react.length > 0) { react.remove(); } if (!resp.empty) { @@ -830,6 +865,73 @@ function initRepository() { issuesTribute.attach($textarea.get()); emojiTribute.attach($textarea.get()); + const $dropzone = $editContentZone.find('.dropzone'); + $dropzone.data("saved", false); + const $files = $editContentZone.find('.comment-files'); + if ($dropzone.length > 0) { + const filenameDict = {}; + $dropzone.dropzone({ + url: $dropzone.data('upload-url'), + headers: {"X-Csrf-Token": csrf}, + maxFiles: $dropzone.data('max-file'), + maxFilesize: $dropzone.data('max-size'), + acceptedFiles: ($dropzone.data('accepts') === '*/*') ? null : $dropzone.data('accepts'), + addRemoveLinks: true, + dictDefaultMessage: $dropzone.data('default-message'), + dictInvalidFileType: $dropzone.data('invalid-input-type'), + dictFileTooBig: $dropzone.data('file-too-big'), + dictRemoveFile: $dropzone.data('remove-file'), + init: function () { + this.on("success", function (file, data) { + filenameDict[file.name] = { + "uuid": data.uuid, + "submitted": false + } + const input = $('').val(data.uuid); + $files.append(input); + }); + this.on("removedfile", function (file) { + if (!(file.name in filenameDict)) { + return; + } + $('#' + filenameDict[file.name].uuid).remove(); + if ($dropzone.data('remove-url') && $dropzone.data('csrf') && !filenameDict[file.name].submitted) { + $.post($dropzone.data('remove-url'), { + file: filenameDict[file.name].uuid, + _csrf: $dropzone.data('csrf') + }); + } + }); + this.on("submit", function () { + $.each(filenameDict, function(name){ + filenameDict[name].submitted = true; + }); + }); + this.on("reload", function (){ + $.getJSON($editContentZone.data('attachment-url'), function(data){ + const drop = $dropzone.get(0).dropzone; + drop.removeAllFiles(true); + $files.empty(); + $.each(data, function(){ + const imgSrc = $dropzone.data('upload-url') + "/" + this.uuid; + drop.emit("addedfile", this); + drop.emit("thumbnail", this, imgSrc); + drop.emit("complete", this); + drop.files.push(this); + filenameDict[this.name] = { + "submitted": true, + "uuid": this.uuid + } + $dropzone.find("img[src='" + imgSrc + "']").css("max-width", "100%"); + const input = $('').val(this.uuid); + $files.append(input); + }); + }); + }); + } + }); + $dropzone.get(0).dropzone.emit("reload"); + } // Give new write/preview data-tab name to distinguish from others const $editContentForm = $editContentZone.find('.ui.comment.form'); const $tabMenu = $editContentForm.find('.tabular.menu'); @@ -845,27 +947,49 @@ function initRepository() { $editContentZone.find('.cancel.button').click(function () { $renderContent.show(); $editContentZone.hide(); + $dropzone.get(0).dropzone.emit("reload"); }); $editContentZone.find('.save.button').click(function () { $renderContent.show(); $editContentZone.hide(); - + const $attachments = $files.find("[name=files]").map(function(){ + return $(this).val(); + }).get(); $.post($editContentZone.data('update-url'), { - "_csrf": csrf, - "content": $textarea.val(), - "context": $editContentZone.data('context') - }, - function (data) { - if (data.length == 0) { - $renderContent.html($('#no-content').html()); - } else { - $renderContent.html(data.content); - emojify.run($renderContent[0]); - $('pre code', $renderContent[0]).each(function () { - hljs.highlightBlock(this); - }); + "_csrf": csrf, + "content": $textarea.val(), + "context": $editContentZone.data('context'), + "files": $attachments + }, + function (data) { + if (data.length == 0) { + $renderContent.html($('#no-content').html()); + } else { + $renderContent.html(data.content); + emojify.run($renderContent[0]); + $('pre code', $renderContent[0]).each(function () { + hljs.highlightBlock(this); + }); + } + const $content = $segment.parent(); + if(!$content.find(".ui.small.images").length){ + if(data.attachments != ""){ + $content.append( + '
' + + '
' + + '
' + + '
' + ); + $content.find(".ui.small.images").html(data.attachments); } - }); + } else if (data.attachments == "") { + $content.find(".ui.small.images").parent().remove(); + } else { + $content.find(".ui.small.images").html(data.attachments); + } + $dropzone.get(0).dropzone.emit("submit"); + $dropzone.get(0).dropzone.emit("reload"); + }); }); } else { $textarea = $segment.find('textarea'); @@ -1447,6 +1571,38 @@ function initEditor() { codeMirrorEditor.setOption("tabSize", editorconfig.tab_width || 4); }); }).trigger('keyup'); + + // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage + // to enable or disable the commit button + const $commitButton = $('#commit-button'); + const $editForm = $('.ui.edit.form'); + const dirtyFileClass = 'dirty-file'; + + // Disabling the button at the start + $commitButton.prop('disabled', true); + + // Registering a custom listener for the file path and the file content + $editForm.areYouSure({ + silent: true, + dirtyClass: dirtyFileClass, + fieldSelector: ':input:not(.commit-form-wrapper :input)', + change: function () { + const dirty = $(this).hasClass(dirtyFileClass); + $commitButton.prop('disabled', !dirty); + } + }); + + $commitButton.click(function (event) { + // A modal which asks if an empty file should be committed + if ($editArea.val().length === 0) { + $('#edit-empty-content-modal').modal({ + onApprove: function () { + $('.edit.form').submit(); + } + }).modal('show'); + event.preventDefault(); + } + }); } function initOrganization() { @@ -2219,6 +2375,7 @@ $(document).ready(function () { initIssueList(); initWipTitle(); initPullRequestReview(); + initRepoStatusChecker(); // Repo clone url. if ($('#repo-clone-url').length > 0) { @@ -3121,8 +3278,41 @@ function initIssueList() { }, fullTextSearch: true - }) - ; + }); + + $(".menu a.label-filter-item").each(function() { + $(this).click(function(e) { + if (e.altKey) { + e.preventDefault(); + + const href = $(this).attr("href"); + const id = $(this).data("label-id"); + + const regStr = "labels=(-?[0-9]+%2c)*(" + id + ")(%2c-?[0-9]+)*&"; + const newStr = "labels=$1-$2$3&"; + + window.location = href.replace(new RegExp(regStr), newStr); + } + }); + }); + + $(".menu .ui.dropdown.label-filter").keydown(function(e) { + if (e.altKey && e.keyCode == 13) { + const selectedItems = $(".menu .ui.dropdown.label-filter .menu .item.selected"); + + if (selectedItems.length > 0) { + const item = $(selectedItems[0]); + + const href = item.attr("href"); + const id = item.data("label-id"); + + const regStr = "labels=(-?[0-9]+%2c)*(" + id + ")(%2c-?[0-9]+)*&"; + const newStr = "labels=$1-$2$3&"; + + window.location = href.replace(new RegExp(regStr), newStr); + } + } + }); } function cancelCodeComment(btn) { const form = $(btn).closest("form"); diff --git a/public/less/_base.less b/public/less/_base.less index e295be368d..1a386f17b0 100644 --- a/public/less/_base.less +++ b/public/less/_base.less @@ -950,6 +950,22 @@ footer { } } +i.icons .icon:first-child { + margin-right: 0; +} + +i.icon.centerlock { + top: 1.5em; +} + +.ui.label > .detail .icons { + margin-right: 0.25em; +} + +.ui.label > .detail .icons .icon { + margin-right: 0; +} + .lines-num { vertical-align: top; text-align: right !important; @@ -1047,7 +1063,7 @@ footer { overflow-x: auto; overflow-y: hidden; - * { + *:not(.fa):not(.octicon):not(.icon) { font-size: 12px; font-family: @monospaced-fonts, monospace; line-height: 20px; diff --git a/public/less/_form.less b/public/less/_form.less index c1a4f80c10..2f37666fb6 100644 --- a/public/less/_form.less +++ b/public/less/_form.less @@ -140,6 +140,16 @@ width: 100% !important; } } + + input[type=number] { + -moz-appearance: textfield; + } + + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } } } diff --git a/public/less/_repository.less b/public/less/_repository.less index ade3477ccc..48a1214c07 100644 --- a/public/less/_repository.less +++ b/public/less/_repository.less @@ -158,6 +158,23 @@ margin: -2px -7px 0 -5px; } + &.labels { + .label-filter .menu .info { + display: inline-block; + padding: 9px 7px 7px 7px; + text-align: center; + border-bottom: 1px solid #cccccc; + font-size: 12px; + + code { + border: 1px solid #cccccc; + border-radius: 3px; + padding: 3px 2px 1px 2px; + font-size: 11px; + } + } + } + .text { margin-left: 0.9em; } @@ -371,9 +388,7 @@ } .file-actions { - margin-top: 0; margin-bottom: -5px; - padding-left: 20px; .btn-octicon { display: inline-block; @@ -1214,6 +1229,15 @@ border-bottom-left-radius: 0; } + &.isSigned.isWarning { + border: 1px solid #db2828; + background: fade(#db2828, 10%); + + .detail.icon { + border-left: 1px solid fade(#db2828, 50%); + } + } + &.isSigned.isVerified { border: 1px solid #21ba45; background: fade(#21ba45, 10%); @@ -1239,21 +1263,6 @@ display: block; } - ol { - clear: both; - padding-left: 0; - margin-top: 5px; - margin-bottom: 28px; - - li { - list-style: none; - padding-bottom: 4px; - margin-bottom: 4px; - border-bottom: 1px dashed #dddddd; - padding-left: 6px; - } - } - span.status { display: inline-block; width: 12px; @@ -1468,6 +1477,34 @@ } } + .diff-stats { + + clear: both; + margin-bottom: 5px; + max-height: 400px; + overflow: auto; + padding-left: 0; + + li { + list-style: none; + padding-bottom: 4px; + margin-bottom: 4px; + border-bottom: 1px dashed #dddddd; + padding-left: 6px; + } + + .diff-counter { + margin-right: 15px; + + .del { + color: red; + } + .add { + color: green; + } + } + } + .repo-search-result { padding-top: 10px; padding-bottom: 10px; @@ -2385,3 +2422,26 @@ tbody.commit-list { padding-top: 8px; padding-bottom: 8px; } + +.issue-keyword { + border-bottom: 1px dotted #959da5; + display: inline-block; +} + +.file-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px !important; +} + +.file-info { + display: flex; + align-items: center; +} + +.file-info-entry + .file-info-entry { + border-left: 1px solid currentColor; + margin-left: 8px; + padding-left: 8px; +} diff --git a/public/vendor/VERSIONS b/public/vendor/VERSIONS index 6c5f10424f..8afae309fe 100644 --- a/public/vendor/VERSIONS +++ b/public/vendor/VERSIONS @@ -42,7 +42,7 @@ File(s): /vendor/plugins/jquery.minicolors/jquery.minicolors.min.js Version: 2.2.3 File(s): /vendor/plugins/codemirror/ -Version: 5.17.0 +Version: 5.49.0 File(s): /vendor/plugins/simplemde/simplemde.min.js Version: 1.10.1 diff --git a/public/vendor/librejs.html b/public/vendor/librejs.html index c17c2f14e8..336cdbb721 100644 --- a/public/vendor/librejs.html +++ b/public/vendor/librejs.html @@ -93,12 +93,12 @@ loadmode.js Expat - codemirror-5.17.0.tar.gz + codemirror-5.49.0.tar.gz meta.js Expat - codemirror-5.17.0.tar.gz + codemirror-5.49.0.tar.gz simplemde.min.js diff --git a/public/vendor/plugins/codemirror/addon/comment/comment.js b/public/vendor/plugins/codemirror/addon/comment/comment.js new file mode 100644 index 0000000000..8394e85a4d --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/comment/comment.js @@ -0,0 +1,209 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var noOptions = {}; + var nonWS = /[^\s\u00a0]/; + var Pos = CodeMirror.Pos; + + function firstNonWS(str) { + var found = str.search(nonWS); + return found == -1 ? 0 : found; + } + + CodeMirror.commands.toggleComment = function(cm) { + cm.toggleComment(); + }; + + CodeMirror.defineExtension("toggleComment", function(options) { + if (!options) options = noOptions; + var cm = this; + var minLine = Infinity, ranges = this.listSelections(), mode = null; + for (var i = ranges.length - 1; i >= 0; i--) { + var from = ranges[i].from(), to = ranges[i].to(); + if (from.line >= minLine) continue; + if (to.line >= minLine) to = Pos(minLine, 0); + minLine = from.line; + if (mode == null) { + if (cm.uncomment(from, to, options)) mode = "un"; + else { cm.lineComment(from, to, options); mode = "line"; } + } else if (mode == "un") { + cm.uncomment(from, to, options); + } else { + cm.lineComment(from, to, options); + } + } + }); + + // Rough heuristic to try and detect lines that are part of multi-line string + function probablyInsideString(cm, pos, line) { + return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line) + } + + function getMode(cm, pos) { + var mode = cm.getMode() + return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos) + } + + CodeMirror.defineExtension("lineComment", function(from, to, options) { + if (!options) options = noOptions; + var self = this, mode = getMode(self, from); + var firstLine = self.getLine(from.line); + if (firstLine == null || probablyInsideString(self, from, firstLine)) return; + + var commentString = options.lineComment || mode.lineComment; + if (!commentString) { + if (options.blockCommentStart || mode.blockCommentStart) { + options.fullLines = true; + self.blockComment(from, to, options); + } + return; + } + + var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1); + var pad = options.padding == null ? " " : options.padding; + var blankLines = options.commentBlankLines || from.line == to.line; + + self.operation(function() { + if (options.indent) { + var baseString = null; + for (var i = from.line; i < end; ++i) { + var line = self.getLine(i); + var whitespace = line.slice(0, firstNonWS(line)); + if (baseString == null || baseString.length > whitespace.length) { + baseString = whitespace; + } + } + for (var i = from.line; i < end; ++i) { + var line = self.getLine(i), cut = baseString.length; + if (!blankLines && !nonWS.test(line)) continue; + if (line.slice(0, cut) != baseString) cut = firstNonWS(line); + self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut)); + } + } else { + for (var i = from.line; i < end; ++i) { + if (blankLines || nonWS.test(self.getLine(i))) + self.replaceRange(commentString + pad, Pos(i, 0)); + } + } + }); + }); + + CodeMirror.defineExtension("blockComment", function(from, to, options) { + if (!options) options = noOptions; + var self = this, mode = getMode(self, from); + var startString = options.blockCommentStart || mode.blockCommentStart; + var endString = options.blockCommentEnd || mode.blockCommentEnd; + if (!startString || !endString) { + if ((options.lineComment || mode.lineComment) && options.fullLines != false) + self.lineComment(from, to, options); + return; + } + if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return + + var end = Math.min(to.line, self.lastLine()); + if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end; + + var pad = options.padding == null ? " " : options.padding; + if (from.line > end) return; + + self.operation(function() { + if (options.fullLines != false) { + var lastLineHasText = nonWS.test(self.getLine(end)); + self.replaceRange(pad + endString, Pos(end)); + self.replaceRange(startString + pad, Pos(from.line, 0)); + var lead = options.blockCommentLead || mode.blockCommentLead; + if (lead != null) for (var i = from.line + 1; i <= end; ++i) + if (i != end || lastLineHasText) + self.replaceRange(lead + pad, Pos(i, 0)); + } else { + self.replaceRange(endString, to); + self.replaceRange(startString, from); + } + }); + }); + + CodeMirror.defineExtension("uncomment", function(from, to, options) { + if (!options) options = noOptions; + var self = this, mode = getMode(self, from); + var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end); + + // Try finding line comments + var lineString = options.lineComment || mode.lineComment, lines = []; + var pad = options.padding == null ? " " : options.padding, didSomething; + lineComment: { + if (!lineString) break lineComment; + for (var i = start; i <= end; ++i) { + var line = self.getLine(i); + var found = line.indexOf(lineString); + if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1; + if (found == -1 && nonWS.test(line)) break lineComment; + if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment; + lines.push(line); + } + self.operation(function() { + for (var i = start; i <= end; ++i) { + var line = lines[i - start]; + var pos = line.indexOf(lineString), endPos = pos + lineString.length; + if (pos < 0) continue; + if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length; + didSomething = true; + self.replaceRange("", Pos(i, pos), Pos(i, endPos)); + } + }); + if (didSomething) return true; + } + + // Try block comments + var startString = options.blockCommentStart || mode.blockCommentStart; + var endString = options.blockCommentEnd || mode.blockCommentEnd; + if (!startString || !endString) return false; + var lead = options.blockCommentLead || mode.blockCommentLead; + var startLine = self.getLine(start), open = startLine.indexOf(startString) + if (open == -1) return false + var endLine = end == start ? startLine : self.getLine(end) + var close = endLine.indexOf(endString, end == start ? open + startString.length : 0); + var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1) + if (close == -1 || + !/comment/.test(self.getTokenTypeAt(insideStart)) || + !/comment/.test(self.getTokenTypeAt(insideEnd)) || + self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1) + return false; + + // Avoid killing block comments completely outside the selection. + // Positions of the last startString before the start of the selection, and the first endString after it. + var lastStart = startLine.lastIndexOf(startString, from.ch); + var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length); + if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false; + // Positions of the first endString after the end of the selection, and the last startString before it. + firstEnd = endLine.indexOf(endString, to.ch); + var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch); + lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart; + if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false; + + self.operation(function() { + self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)), + Pos(end, close + endString.length)); + var openEnd = open + startString.length; + if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length; + self.replaceRange("", Pos(start, open), Pos(start, openEnd)); + if (lead) for (var i = start + 1; i <= end; ++i) { + var line = self.getLine(i), found = line.indexOf(lead); + if (found == -1 || nonWS.test(line.slice(0, found))) continue; + var foundEnd = found + lead.length; + if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length; + self.replaceRange("", Pos(i, found), Pos(i, foundEnd)); + } + }); + return true; + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/comment/continuecomment.js b/public/vendor/plugins/codemirror/addon/comment/continuecomment.js new file mode 100644 index 0000000000..a5f957b73b --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/comment/continuecomment.js @@ -0,0 +1,78 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + function continueComment(cm) { + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(), mode, inserts = []; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].head + if (!/\bcomment\b/.test(cm.getTokenTypeAt(pos))) return CodeMirror.Pass; + var modeHere = cm.getModeAt(pos) + if (!mode) mode = modeHere; + else if (mode != modeHere) return CodeMirror.Pass; + + var insert = null; + if (mode.blockCommentStart && mode.blockCommentContinue) { + var line = cm.getLine(pos.line).slice(0, pos.ch) + var end = line.lastIndexOf(mode.blockCommentEnd), found + if (end != -1 && end == pos.ch - mode.blockCommentEnd.length) { + // Comment ended, don't continue it + } else if ((found = line.lastIndexOf(mode.blockCommentStart)) > -1 && found > end) { + insert = line.slice(0, found) + if (/\S/.test(insert)) { + insert = "" + for (var j = 0; j < found; ++j) insert += " " + } + } else if ((found = line.indexOf(mode.blockCommentContinue)) > -1 && !/\S/.test(line.slice(0, found))) { + insert = line.slice(0, found) + } + if (insert != null) insert += mode.blockCommentContinue + } + if (insert == null && mode.lineComment && continueLineCommentEnabled(cm)) { + var line = cm.getLine(pos.line), found = line.indexOf(mode.lineComment); + if (found > -1) { + insert = line.slice(0, found); + if (/\S/.test(insert)) insert = null; + else insert += mode.lineComment + line.slice(found + mode.lineComment.length).match(/^\s*/)[0]; + } + } + if (insert == null) return CodeMirror.Pass; + inserts[i] = "\n" + insert; + } + + cm.operation(function() { + for (var i = ranges.length - 1; i >= 0; i--) + cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert"); + }); + } + + function continueLineCommentEnabled(cm) { + var opt = cm.getOption("continueComments"); + if (opt && typeof opt == "object") + return opt.continueLineComment !== false; + return true; + } + + CodeMirror.defineOption("continueComments", null, function(cm, val, prev) { + if (prev && prev != CodeMirror.Init) + cm.removeKeyMap("continueComment"); + if (val) { + var key = "Enter"; + if (typeof val == "string") + key = val; + else if (typeof val == "object" && val.key) + key = val.key; + var map = {name: "continueComment"}; + map[key] = continueComment; + cm.addKeyMap(map); + } + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/dialog/dialog.css b/public/vendor/plugins/codemirror/addon/dialog/dialog.css new file mode 100644 index 0000000000..677c078387 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/dialog/dialog.css @@ -0,0 +1,32 @@ +.CodeMirror-dialog { + position: absolute; + left: 0; right: 0; + background: inherit; + z-index: 15; + padding: .1em .8em; + overflow: hidden; + color: inherit; +} + +.CodeMirror-dialog-top { + border-bottom: 1px solid #eee; + top: 0; +} + +.CodeMirror-dialog-bottom { + border-top: 1px solid #eee; + bottom: 0; +} + +.CodeMirror-dialog input { + border: none; + outline: none; + background: transparent; + width: 20em; + color: inherit; + font-family: monospace; +} + +.CodeMirror-dialog button { + font-size: 70%; +} diff --git a/public/vendor/plugins/codemirror/addon/dialog/dialog.js b/public/vendor/plugins/codemirror/addon/dialog/dialog.js new file mode 100644 index 0000000000..23b06a8323 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/dialog/dialog.js @@ -0,0 +1,161 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Open simple dialogs on top of an editor. Relies on dialog.css. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + function dialogDiv(cm, template, bottom) { + var wrap = cm.getWrapperElement(); + var dialog; + dialog = wrap.appendChild(document.createElement("div")); + if (bottom) + dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom"; + else + dialog.className = "CodeMirror-dialog CodeMirror-dialog-top"; + + if (typeof template == "string") { + dialog.innerHTML = template; + } else { // Assuming it's a detached DOM element. + dialog.appendChild(template); + } + CodeMirror.addClass(wrap, 'dialog-opened'); + return dialog; + } + + function closeNotification(cm, newVal) { + if (cm.state.currentNotificationClose) + cm.state.currentNotificationClose(); + cm.state.currentNotificationClose = newVal; + } + + CodeMirror.defineExtension("openDialog", function(template, callback, options) { + if (!options) options = {}; + + closeNotification(this, null); + + var dialog = dialogDiv(this, template, options.bottom); + var closed = false, me = this; + function close(newVal) { + if (typeof newVal == 'string') { + inp.value = newVal; + } else { + if (closed) return; + closed = true; + CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); + dialog.parentNode.removeChild(dialog); + me.focus(); + + if (options.onClose) options.onClose(dialog); + } + } + + var inp = dialog.getElementsByTagName("input")[0], button; + if (inp) { + inp.focus(); + + if (options.value) { + inp.value = options.value; + if (options.selectValueOnOpen !== false) { + inp.select(); + } + } + + if (options.onInput) + CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);}); + if (options.onKeyUp) + CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);}); + + CodeMirror.on(inp, "keydown", function(e) { + if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; } + if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) { + inp.blur(); + CodeMirror.e_stop(e); + close(); + } + if (e.keyCode == 13) callback(inp.value, e); + }); + + if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close); + } else if (button = dialog.getElementsByTagName("button")[0]) { + CodeMirror.on(button, "click", function() { + close(); + me.focus(); + }); + + if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close); + + button.focus(); + } + return close; + }); + + CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) { + closeNotification(this, null); + var dialog = dialogDiv(this, template, options && options.bottom); + var buttons = dialog.getElementsByTagName("button"); + var closed = false, me = this, blurring = 1; + function close() { + if (closed) return; + closed = true; + CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); + dialog.parentNode.removeChild(dialog); + me.focus(); + } + buttons[0].focus(); + for (var i = 0; i < buttons.length; ++i) { + var b = buttons[i]; + (function(callback) { + CodeMirror.on(b, "click", function(e) { + CodeMirror.e_preventDefault(e); + close(); + if (callback) callback(me); + }); + })(callbacks[i]); + CodeMirror.on(b, "blur", function() { + --blurring; + setTimeout(function() { if (blurring <= 0) close(); }, 200); + }); + CodeMirror.on(b, "focus", function() { ++blurring; }); + } + }); + + /* + * openNotification + * Opens a notification, that can be closed with an optional timer + * (default 5000ms timer) and always closes on click. + * + * If a notification is opened while another is opened, it will close the + * currently opened one and open the new one immediately. + */ + CodeMirror.defineExtension("openNotification", function(template, options) { + closeNotification(this, close); + var dialog = dialogDiv(this, template, options && options.bottom); + var closed = false, doneTimer; + var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000; + + function close() { + if (closed) return; + closed = true; + clearTimeout(doneTimer); + CodeMirror.rmClass(dialog.parentNode, 'dialog-opened'); + dialog.parentNode.removeChild(dialog); + } + + CodeMirror.on(dialog, 'click', function(e) { + CodeMirror.e_preventDefault(e); + close(); + }); + + if (duration) + doneTimer = setTimeout(close, duration); + + return close; + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/display/autorefresh.js b/public/vendor/plugins/codemirror/addon/display/autorefresh.js new file mode 100644 index 0000000000..37014dc31d --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/display/autorefresh.js @@ -0,0 +1,47 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")) + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod) + else // Plain browser env + mod(CodeMirror) +})(function(CodeMirror) { + "use strict" + + CodeMirror.defineOption("autoRefresh", false, function(cm, val) { + if (cm.state.autoRefresh) { + stopListening(cm, cm.state.autoRefresh) + cm.state.autoRefresh = null + } + if (val && cm.display.wrapper.offsetHeight == 0) + startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250}) + }) + + function startListening(cm, state) { + function check() { + if (cm.display.wrapper.offsetHeight) { + stopListening(cm, state) + if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight) + cm.refresh() + } else { + state.timeout = setTimeout(check, state.delay) + } + } + state.timeout = setTimeout(check, state.delay) + state.hurry = function() { + clearTimeout(state.timeout) + state.timeout = setTimeout(check, 50) + } + CodeMirror.on(window, "mouseup", state.hurry) + CodeMirror.on(window, "keyup", state.hurry) + } + + function stopListening(_cm, state) { + clearTimeout(state.timeout) + CodeMirror.off(window, "mouseup", state.hurry) + CodeMirror.off(window, "keyup", state.hurry) + } +}); diff --git a/public/vendor/plugins/codemirror/addon/display/fullscreen.css b/public/vendor/plugins/codemirror/addon/display/fullscreen.css new file mode 100644 index 0000000000..437acd89be --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/display/fullscreen.css @@ -0,0 +1,6 @@ +.CodeMirror-fullscreen { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + height: auto; + z-index: 9; +} diff --git a/public/vendor/plugins/codemirror/addon/display/fullscreen.js b/public/vendor/plugins/codemirror/addon/display/fullscreen.js new file mode 100644 index 0000000000..eda7300f12 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/display/fullscreen.js @@ -0,0 +1,41 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("fullScreen", false, function(cm, val, old) { + if (old == CodeMirror.Init) old = false; + if (!old == !val) return; + if (val) setFullscreen(cm); + else setNormal(cm); + }); + + function setFullscreen(cm) { + var wrap = cm.getWrapperElement(); + cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset, + width: wrap.style.width, height: wrap.style.height}; + wrap.style.width = ""; + wrap.style.height = "auto"; + wrap.className += " CodeMirror-fullscreen"; + document.documentElement.style.overflow = "hidden"; + cm.refresh(); + } + + function setNormal(cm) { + var wrap = cm.getWrapperElement(); + wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, ""); + document.documentElement.style.overflow = ""; + var info = cm.state.fullScreenRestore; + wrap.style.width = info.width; wrap.style.height = info.height; + window.scrollTo(info.scrollLeft, info.scrollTop); + cm.refresh(); + } +}); diff --git a/public/vendor/plugins/codemirror/addon/display/panel.js b/public/vendor/plugins/codemirror/addon/display/panel.js new file mode 100644 index 0000000000..5faf1d560e --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/display/panel.js @@ -0,0 +1,127 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + CodeMirror.defineExtension("addPanel", function(node, options) { + options = options || {}; + + if (!this.state.panels) initPanels(this); + + var info = this.state.panels; + var wrapper = info.wrapper; + var cmWrapper = this.getWrapperElement(); + var replace = options.replace instanceof Panel && !options.replace.cleared; + + if (options.after instanceof Panel && !options.after.cleared) { + wrapper.insertBefore(node, options.before.node.nextSibling); + } else if (options.before instanceof Panel && !options.before.cleared) { + wrapper.insertBefore(node, options.before.node); + } else if (replace) { + wrapper.insertBefore(node, options.replace.node); + info.panels++; + options.replace.clear(); + } else if (options.position == "bottom") { + wrapper.appendChild(node); + } else if (options.position == "before-bottom") { + wrapper.insertBefore(node, cmWrapper.nextSibling); + } else if (options.position == "after-top") { + wrapper.insertBefore(node, cmWrapper); + } else { + wrapper.insertBefore(node, wrapper.firstChild); + } + + var height = (options && options.height) || node.offsetHeight; + this._setSize(null, info.heightLeft -= height); + if (!replace) { + info.panels++; + } + if (options.stable && isAtTop(this, node)) + this.scrollTo(null, this.getScrollInfo().top + height) + + return new Panel(this, node, options, height); + }); + + function Panel(cm, node, options, height) { + this.cm = cm; + this.node = node; + this.options = options; + this.height = height; + this.cleared = false; + } + + Panel.prototype.clear = function() { + if (this.cleared) return; + this.cleared = true; + var info = this.cm.state.panels; + this.cm._setSize(null, info.heightLeft += this.height); + if (this.options.stable && isAtTop(this.cm, this.node)) + this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height) + info.wrapper.removeChild(this.node); + if (--info.panels == 0) removePanels(this.cm); + }; + + Panel.prototype.changed = function(height) { + var newHeight = height == null ? this.node.offsetHeight : height; + var info = this.cm.state.panels; + this.cm._setSize(null, info.heightLeft -= (newHeight - this.height)); + this.height = newHeight; + }; + + function initPanels(cm) { + var wrap = cm.getWrapperElement(); + var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle; + var height = parseInt(style.height); + var info = cm.state.panels = { + setHeight: wrap.style.height, + heightLeft: height, + panels: 0, + wrapper: document.createElement("div") + }; + wrap.parentNode.insertBefore(info.wrapper, wrap); + var hasFocus = cm.hasFocus(); + info.wrapper.appendChild(wrap); + if (hasFocus) cm.focus(); + + cm._setSize = cm.setSize; + if (height != null) cm.setSize = function(width, newHeight) { + if (newHeight == null) return this._setSize(width, newHeight); + info.setHeight = newHeight; + if (typeof newHeight != "number") { + var px = /^(\d+\.?\d*)px$/.exec(newHeight); + if (px) { + newHeight = Number(px[1]); + } else { + info.wrapper.style.height = newHeight; + newHeight = info.wrapper.offsetHeight; + info.wrapper.style.height = ""; + } + } + cm._setSize(width, info.heightLeft += (newHeight - height)); + height = newHeight; + }; + } + + function removePanels(cm) { + var info = cm.state.panels; + cm.state.panels = null; + + var wrap = cm.getWrapperElement(); + info.wrapper.parentNode.replaceChild(wrap, info.wrapper); + wrap.style.height = info.setHeight; + cm.setSize = cm._setSize; + cm.setSize(); + } + + function isAtTop(cm, dom) { + for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling) + if (sibling == cm.getWrapperElement()) return true + return false + } +}); diff --git a/public/vendor/plugins/codemirror/addon/display/placeholder.js b/public/vendor/plugins/codemirror/addon/display/placeholder.js new file mode 100644 index 0000000000..4eabe3d901 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/display/placeholder.js @@ -0,0 +1,63 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + CodeMirror.defineOption("placeholder", "", function(cm, val, old) { + var prev = old && old != CodeMirror.Init; + if (val && !prev) { + cm.on("blur", onBlur); + cm.on("change", onChange); + cm.on("swapDoc", onChange); + onChange(cm); + } else if (!val && prev) { + cm.off("blur", onBlur); + cm.off("change", onChange); + cm.off("swapDoc", onChange); + clearPlaceholder(cm); + var wrapper = cm.getWrapperElement(); + wrapper.className = wrapper.className.replace(" CodeMirror-empty", ""); + } + + if (val && !cm.hasFocus()) onBlur(cm); + }); + + function clearPlaceholder(cm) { + if (cm.state.placeholder) { + cm.state.placeholder.parentNode.removeChild(cm.state.placeholder); + cm.state.placeholder = null; + } + } + function setPlaceholder(cm) { + clearPlaceholder(cm); + var elt = cm.state.placeholder = document.createElement("pre"); + elt.style.cssText = "height: 0; overflow: visible"; + elt.style.direction = cm.getOption("direction"); + elt.className = "CodeMirror-placeholder CodeMirror-line-like"; + var placeHolder = cm.getOption("placeholder") + if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder) + elt.appendChild(placeHolder) + cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild); + } + + function onBlur(cm) { + if (isEmpty(cm)) setPlaceholder(cm); + } + function onChange(cm) { + var wrapper = cm.getWrapperElement(), empty = isEmpty(cm); + wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : ""); + + if (empty) setPlaceholder(cm); + else clearPlaceholder(cm); + } + + function isEmpty(cm) { + return (cm.lineCount() === 1) && (cm.getLine(0) === ""); + } +}); diff --git a/public/vendor/plugins/codemirror/addon/display/rulers.js b/public/vendor/plugins/codemirror/addon/display/rulers.js new file mode 100644 index 0000000000..0bb83bb022 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/display/rulers.js @@ -0,0 +1,51 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("rulers", false, function(cm, val) { + if (cm.state.rulerDiv) { + cm.state.rulerDiv.parentElement.removeChild(cm.state.rulerDiv) + cm.state.rulerDiv = null + cm.off("refresh", drawRulers) + } + if (val && val.length) { + cm.state.rulerDiv = cm.display.lineSpace.parentElement.insertBefore(document.createElement("div"), cm.display.lineSpace) + cm.state.rulerDiv.className = "CodeMirror-rulers" + drawRulers(cm) + cm.on("refresh", drawRulers) + } + }); + + function drawRulers(cm) { + cm.state.rulerDiv.textContent = "" + var val = cm.getOption("rulers"); + var cw = cm.defaultCharWidth(); + var left = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), "div").left; + cm.state.rulerDiv.style.minHeight = (cm.display.scroller.offsetHeight + 30) + "px"; + for (var i = 0; i < val.length; i++) { + var elt = document.createElement("div"); + elt.className = "CodeMirror-ruler"; + var col, conf = val[i]; + if (typeof conf == "number") { + col = conf; + } else { + col = conf.column; + if (conf.className) elt.className += " " + conf.className; + if (conf.color) elt.style.borderColor = conf.color; + if (conf.lineStyle) elt.style.borderLeftStyle = conf.lineStyle; + if (conf.width) elt.style.borderLeftWidth = conf.width; + } + elt.style.left = (left + col * cw) + "px"; + cm.state.rulerDiv.appendChild(elt) + } + } +}); diff --git a/public/vendor/plugins/codemirror/addon/edit/closebrackets.js b/public/vendor/plugins/codemirror/addon/edit/closebrackets.js new file mode 100644 index 0000000000..4415c39381 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/edit/closebrackets.js @@ -0,0 +1,191 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var defaults = { + pairs: "()[]{}''\"\"", + closeBefore: ")]}'\":;>", + triples: "", + explode: "[]{}" + }; + + var Pos = CodeMirror.Pos; + + CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.removeKeyMap(keyMap); + cm.state.closeBrackets = null; + } + if (val) { + ensureBound(getOption(val, "pairs")) + cm.state.closeBrackets = val; + cm.addKeyMap(keyMap); + } + }); + + function getOption(conf, name) { + if (name == "pairs" && typeof conf == "string") return conf; + if (typeof conf == "object" && conf[name] != null) return conf[name]; + return defaults[name]; + } + + var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; + function ensureBound(chars) { + for (var i = 0; i < chars.length; i++) { + var ch = chars.charAt(i), key = "'" + ch + "'" + if (!keyMap[key]) keyMap[key] = handler(ch) + } + } + ensureBound(defaults.pairs + "`") + + function handler(ch) { + return function(cm) { return handleChar(cm, ch); }; + } + + function getConfig(cm) { + var deflt = cm.state.closeBrackets; + if (!deflt || deflt.override) return deflt; + var mode = cm.getModeAt(cm.getCursor()); + return mode.closeBrackets || deflt; + } + + function handleBackspace(cm) { + var conf = getConfig(cm); + if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; + + var pairs = getOption(conf, "pairs"); + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + for (var i = ranges.length - 1; i >= 0; i--) { + var cur = ranges[i].head; + cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete"); + } + } + + function handleEnter(cm) { + var conf = getConfig(cm); + var explode = conf && getOption(conf, "explode"); + if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; + + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + cm.operation(function() { + var linesep = cm.lineSeparator() || "\n"; + cm.replaceSelection(linesep + linesep, null); + cm.execCommand("goCharLeft"); + ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var line = ranges[i].head.line; + cm.indentLine(line, null, true); + cm.indentLine(line + 1, null, true); + } + }); + } + + function contractSelection(sel) { + var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0; + return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)), + head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))}; + } + + function handleChar(cm, ch) { + var conf = getConfig(cm); + if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; + + var pairs = getOption(conf, "pairs"); + var pos = pairs.indexOf(ch); + if (pos == -1) return CodeMirror.Pass; + + var closeBefore = getOption(conf,"closeBefore"); + + var triples = getOption(conf, "triples"); + + var identical = pairs.charAt(pos + 1) == ch; + var ranges = cm.listSelections(); + var opening = pos % 2 == 0; + + var type; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i], cur = range.head, curType; + var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); + if (opening && !range.empty()) { + curType = "surround"; + } else if ((identical || !opening) && next == ch) { + if (identical && stringStartsAfter(cm, cur)) + curType = "both"; + else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) + curType = "skipThree"; + else + curType = "skip"; + } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && + cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) { + if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass; + curType = "addFour"; + } else if (identical) { + var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur) + if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both"; + else return CodeMirror.Pass; + } else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) { + curType = "both"; + } else { + return CodeMirror.Pass; + } + if (!type) type = curType; + else if (type != curType) return CodeMirror.Pass; + } + + var left = pos % 2 ? pairs.charAt(pos - 1) : ch; + var right = pos % 2 ? ch : pairs.charAt(pos + 1); + cm.operation(function() { + if (type == "skip") { + cm.execCommand("goCharRight"); + } else if (type == "skipThree") { + for (var i = 0; i < 3; i++) + cm.execCommand("goCharRight"); + } else if (type == "surround") { + var sels = cm.getSelections(); + for (var i = 0; i < sels.length; i++) + sels[i] = left + sels[i] + right; + cm.replaceSelections(sels, "around"); + sels = cm.listSelections().slice(); + for (var i = 0; i < sels.length; i++) + sels[i] = contractSelection(sels[i]); + cm.setSelections(sels); + } else if (type == "both") { + cm.replaceSelection(left + right, null); + cm.triggerElectric(left + right); + cm.execCommand("goCharLeft"); + } else if (type == "addFour") { + cm.replaceSelection(left + left + left + left, "before"); + cm.execCommand("goCharRight"); + } + }); + } + + function charsAround(cm, pos) { + var str = cm.getRange(Pos(pos.line, pos.ch - 1), + Pos(pos.line, pos.ch + 1)); + return str.length == 2 ? str : null; + } + + function stringStartsAfter(cm, pos) { + var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1)) + return /\bstring/.test(token.type) && token.start == pos.ch && + (pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos))) + } +}); diff --git a/public/vendor/plugins/codemirror/addon/edit/closetag.js b/public/vendor/plugins/codemirror/addon/edit/closetag.js new file mode 100644 index 0000000000..4f5aae49d9 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/edit/closetag.js @@ -0,0 +1,184 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +/** + * Tag-closer extension for CodeMirror. + * + * This extension adds an "autoCloseTags" option that can be set to + * either true to get the default behavior, or an object to further + * configure its behavior. + * + * These are supported options: + * + * `whenClosing` (default true) + * Whether to autoclose when the '/' of a closing tag is typed. + * `whenOpening` (default true) + * Whether to autoclose the tag when the final '>' of an opening + * tag is typed. + * `dontCloseTags` (default is empty tags for HTML, none for XML) + * An array of tag names that should not be autoclosed. + * `indentTags` (default is block tags for HTML, none for XML) + * An array of tag names that should, when opened, cause a + * blank line to be added inside the tag, and the blank line and + * closing line to be indented. + * `emptyTags` (default is none) + * An array of XML tag names that should be autoclosed with '/>'. + * + * See demos/closetag.html for a usage example. + */ + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../fold/xml-fold")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../fold/xml-fold"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) { + if (old != CodeMirror.Init && old) + cm.removeKeyMap("autoCloseTags"); + if (!val) return; + var map = {name: "autoCloseTags"}; + if (typeof val != "object" || val.whenClosing) + map["'/'"] = function(cm) { return autoCloseSlash(cm); }; + if (typeof val != "object" || val.whenOpening) + map["'>'"] = function(cm) { return autoCloseGT(cm); }; + cm.addKeyMap(map); + }); + + var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", + "source", "track", "wbr"]; + var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4", + "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"]; + + function autoCloseGT(cm) { + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(), replacements = []; + var opt = cm.getOption("autoCloseTags"); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var pos = ranges[i].head, tok = cm.getTokenAt(pos); + var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; + var tagInfo = inner.mode.xmlCurrentTag && inner.mode.xmlCurrentTag(state) + var tagName = tagInfo && tagInfo.name + if (!tagName) return CodeMirror.Pass + + var html = inner.mode.configuration == "html"; + var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose); + var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent); + + if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch); + var lowerTagName = tagName.toLowerCase(); + // Don't process the '>' at the end of an end-tag or self-closing tag + if (!tagName || + tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) || + tok.type == "tag" && tagInfo.close || + tok.string.indexOf("/") == (tok.string.length - 1) || // match something like + dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || + closingTagExists(cm, inner.mode.xmlCurrentContext && inner.mode.xmlCurrentContext(state) || [], tagName, pos, true)) + return CodeMirror.Pass; + + var emptyTags = typeof opt == "object" && opt.emptyTags; + if (emptyTags && indexOf(emptyTags, tagName) > -1) { + replacements[i] = { text: "/>", newPos: CodeMirror.Pos(pos.line, pos.ch + 2) }; + continue; + } + + var indent = indentTags && indexOf(indentTags, lowerTagName) > -1; + replacements[i] = {indent: indent, + text: ">" + (indent ? "\n\n" : "") + "", + newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)}; + } + + var dontIndentOnAutoClose = (typeof opt == "object" && opt.dontIndentOnAutoClose); + for (var i = ranges.length - 1; i >= 0; i--) { + var info = replacements[i]; + cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert"); + var sel = cm.listSelections().slice(0); + sel[i] = {head: info.newPos, anchor: info.newPos}; + cm.setSelections(sel); + if (!dontIndentOnAutoClose && info.indent) { + cm.indentLine(info.newPos.line, null, true); + cm.indentLine(info.newPos.line + 1, null, true); + } + } + } + + function autoCloseCurrent(cm, typingSlash) { + var ranges = cm.listSelections(), replacements = []; + var head = typingSlash ? "/" : "") replacement += ">"; + replacements[i] = replacement; + } + cm.replaceSelections(replacements); + ranges = cm.listSelections(); + if (!dontIndentOnAutoClose) { + for (var i = 0; i < ranges.length; i++) + if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line) + cm.indentLine(ranges[i].head.line); + } + } + + function autoCloseSlash(cm) { + if (cm.getOption("disableInput")) return CodeMirror.Pass; + return autoCloseCurrent(cm, true); + } + + CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); }; + + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + + // If xml-fold is loaded, we use its functionality to try and verify + // whether a given tag is actually unclosed. + function closingTagExists(cm, context, tagName, pos, newTag) { + if (!CodeMirror.scanForClosingTag) return false; + var end = Math.min(cm.lastLine() + 1, pos.line + 500); + var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end); + if (!nextClose || nextClose.tag != tagName) return false; + // If the immediate wrapping context contains onCx instances of + // the same tag, a closing tag only exists if there are at least + // that many closing tags of that type following. + var onCx = newTag ? 1 : 0 + for (var i = context.length - 1; i >= 0; i--) { + if (context[i] == tagName) ++onCx + else break + } + pos = nextClose.to; + for (var i = 1; i < onCx; i++) { + var next = CodeMirror.scanForClosingTag(cm, pos, null, end); + if (!next || next.tag != tagName) return false; + pos = next.to; + } + return true; + } +}); diff --git a/public/vendor/plugins/codemirror/addon/edit/continuelist.js b/public/vendor/plugins/codemirror/addon/edit/continuelist.js new file mode 100644 index 0000000000..fb5f03735d --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/edit/continuelist.js @@ -0,0 +1,99 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/, + emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/, + unorderedListRE = /[*+-]\s/; + + CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) { + if (cm.getOption("disableInput")) return CodeMirror.Pass; + var ranges = cm.listSelections(), replacements = []; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].head; + + // If we're not in Markdown mode, fall back to normal newlineAndIndent + var eolState = cm.getStateAfter(pos.line); + var inner = CodeMirror.innerMode(cm.getMode(), eolState); + if (inner.mode.name !== "markdown") { + cm.execCommand("newlineAndIndent"); + return; + } else { + eolState = inner.state; + } + + var inList = eolState.list !== false; + var inQuote = eolState.quote !== 0; + + var line = cm.getLine(pos.line), match = listRE.exec(line); + var cursorBeforeBullet = /^\s*$/.test(line.slice(0, pos.ch)); + if (!ranges[i].empty() || (!inList && !inQuote) || !match || cursorBeforeBullet) { + cm.execCommand("newlineAndIndent"); + return; + } + if (emptyListRE.test(line)) { + if (!/>\s*$/.test(line)) cm.replaceRange("", { + line: pos.line, ch: 0 + }, { + line: pos.line, ch: pos.ch + 1 + }); + replacements[i] = "\n"; + } else { + var indent = match[1], after = match[5]; + var numbered = !(unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0); + var bullet = numbered ? (parseInt(match[3], 10) + 1) + match[4] : match[2].replace("x", " "); + replacements[i] = "\n" + indent + bullet + after; + + if (numbered) incrementRemainingMarkdownListNumbers(cm, pos); + } + } + + cm.replaceSelections(replacements); + }; + + // Auto-updating Markdown list numbers when a new item is added to the + // middle of a list + function incrementRemainingMarkdownListNumbers(cm, pos) { + var startLine = pos.line, lookAhead = 0, skipCount = 0; + var startItem = listRE.exec(cm.getLine(startLine)), startIndent = startItem[1]; + + do { + lookAhead += 1; + var nextLineNumber = startLine + lookAhead; + var nextLine = cm.getLine(nextLineNumber), nextItem = listRE.exec(nextLine); + + if (nextItem) { + var nextIndent = nextItem[1]; + var newNumber = (parseInt(startItem[3], 10) + lookAhead - skipCount); + var nextNumber = (parseInt(nextItem[3], 10)), itemNumber = nextNumber; + + if (startIndent === nextIndent && !isNaN(nextNumber)) { + if (newNumber === nextNumber) itemNumber = nextNumber + 1; + if (newNumber > nextNumber) itemNumber = newNumber + 1; + cm.replaceRange( + nextLine.replace(listRE, nextIndent + itemNumber + nextItem[4] + nextItem[5]), + { + line: nextLineNumber, ch: 0 + }, { + line: nextLineNumber, ch: nextLine.length + }); + } else { + if (startIndent.length > nextIndent.length) return; + // This doesn't run if the next line immediatley indents, as it is + // not clear of the users intention (new indented item or same level) + if ((startIndent.length < nextIndent.length) && (lookAhead === 1)) return; + skipCount += 1; + } + } + } while (nextItem); + } +}); diff --git a/public/vendor/plugins/codemirror/addon/edit/matchbrackets.js b/public/vendor/plugins/codemirror/addon/edit/matchbrackets.js new file mode 100644 index 0000000000..2a147282c4 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/edit/matchbrackets.js @@ -0,0 +1,150 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && + (document.documentMode == null || document.documentMode < 8); + + var Pos = CodeMirror.Pos; + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<", "<": ">>", ">": "<<"}; + + function bracketRegex(config) { + return config && config.bracketRegex || /[(){}[\]]/ + } + + function findMatchingBracket(cm, where, config) { + var line = cm.getLineHandle(where.line), pos = where.ch - 1; + var afterCursor = config && config.afterCursor + if (afterCursor == null) + afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className) + var re = bracketRegex(config) + + // A cursor is defined as between two characters, but in in vim command mode + // (i.e. not insert mode), the cursor is visually represented as a + // highlighted box on top of the 2nd character. Otherwise, we allow matches + // from before or after the cursor. + var match = (!afterCursor && pos >= 0 && re.test(line.text.charAt(pos)) && matching[line.text.charAt(pos)]) || + re.test(line.text.charAt(pos + 1)) && matching[line.text.charAt(++pos)]; + if (!match) return null; + var dir = match.charAt(1) == ">" ? 1 : -1; + if (config && config.strict && (dir > 0) != (pos == where.ch)) return null; + var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); + + var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); + if (found == null) return null; + return {from: Pos(where.line, pos), to: found && found.pos, + match: found && found.ch == match.charAt(0), forward: dir > 0}; + } + + // bracketRegex is used to specify which type of bracket to scan + // should be a regexp, e.g. /[[\]]/ + // + // Note: If "where" is on an open bracket, then this bracket is ignored. + // + // Returns false when no bracket was found, null when it reached + // maxScanLines and gave up + function scanForBracket(cm, where, dir, style, config) { + var maxScanLen = (config && config.maxScanLineLength) || 10000; + var maxScanLines = (config && config.maxScanLines) || 1000; + + var stack = []; + var re = bracketRegex(config) + var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) + : Math.max(cm.firstLine() - 1, where.line - maxScanLines); + for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { + var line = cm.getLine(lineNo); + if (!line) continue; + var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; + if (line.length > maxScanLen) continue; + if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); + for (; pos != end; pos += dir) { + var ch = line.charAt(pos); + if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { + var match = matching[ch]; + if (match && (match.charAt(1) == ">") == (dir > 0)) stack.push(ch); + else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; + else stack.pop(); + } + } + } + return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; + } + + function matchBrackets(cm, autoclear, config) { + // Disable brace matching in long lines, since it'll cause hugely slow updates + var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; + var marks = [], ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config); + if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { + var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); + if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) + marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); + } + } + + if (marks.length) { + // Kludge to work around the IE bug from issue #1193, where text + // input stops going to the textare whever this fires. + if (ie_lt8 && cm.state.focused) cm.focus(); + + var clear = function() { + cm.operation(function() { + for (var i = 0; i < marks.length; i++) marks[i].clear(); + }); + }; + if (autoclear) setTimeout(clear, 800); + else return clear; + } + } + + function doMatchBrackets(cm) { + cm.operation(function() { + if (cm.state.matchBrackets.currentlyHighlighted) { + cm.state.matchBrackets.currentlyHighlighted(); + cm.state.matchBrackets.currentlyHighlighted = null; + } + cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); + }); + } + + CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.off("cursorActivity", doMatchBrackets); + if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) { + cm.state.matchBrackets.currentlyHighlighted(); + cm.state.matchBrackets.currentlyHighlighted = null; + } + } + if (val) { + cm.state.matchBrackets = typeof val == "object" ? val : {}; + cm.on("cursorActivity", doMatchBrackets); + } + }); + + CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); + CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){ + // Backwards-compatibility kludge + if (oldConfig || typeof config == "boolean") { + if (!oldConfig) { + config = config ? {strict: true} : null + } else { + oldConfig.strict = config + config = oldConfig + } + } + return findMatchingBracket(this, pos, config) + }); + CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ + return scanForBracket(this, pos, dir, style, config); + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/edit/matchtags.js b/public/vendor/plugins/codemirror/addon/edit/matchtags.js new file mode 100644 index 0000000000..2203d9390d --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/edit/matchtags.js @@ -0,0 +1,66 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../fold/xml-fold")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../fold/xml-fold"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("matchTags", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.off("cursorActivity", doMatchTags); + cm.off("viewportChange", maybeUpdateMatch); + clear(cm); + } + if (val) { + cm.state.matchBothTags = typeof val == "object" && val.bothTags; + cm.on("cursorActivity", doMatchTags); + cm.on("viewportChange", maybeUpdateMatch); + doMatchTags(cm); + } + }); + + function clear(cm) { + if (cm.state.tagHit) cm.state.tagHit.clear(); + if (cm.state.tagOther) cm.state.tagOther.clear(); + cm.state.tagHit = cm.state.tagOther = null; + } + + function doMatchTags(cm) { + cm.state.failedTagMatch = false; + cm.operation(function() { + clear(cm); + if (cm.somethingSelected()) return; + var cur = cm.getCursor(), range = cm.getViewport(); + range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to); + var match = CodeMirror.findMatchingTag(cm, cur, range); + if (!match) return; + if (cm.state.matchBothTags) { + var hit = match.at == "open" ? match.open : match.close; + if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"}); + } + var other = match.at == "close" ? match.open : match.close; + if (other) + cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"}); + else + cm.state.failedTagMatch = true; + }); + } + + function maybeUpdateMatch(cm) { + if (cm.state.failedTagMatch) doMatchTags(cm); + } + + CodeMirror.commands.toMatchingTag = function(cm) { + var found = CodeMirror.findMatchingTag(cm, cm.getCursor()); + if (found) { + var other = found.at == "close" ? found.open : found.close; + if (other) cm.extendSelection(other.to, other.from); + } + }; +}); diff --git a/public/vendor/plugins/codemirror/addon/edit/trailingspace.js b/public/vendor/plugins/codemirror/addon/edit/trailingspace.js new file mode 100644 index 0000000000..c39c310a99 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/edit/trailingspace.js @@ -0,0 +1,27 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) { + if (prev == CodeMirror.Init) prev = false; + if (prev && !val) + cm.removeOverlay("trailingspace"); + else if (!prev && val) + cm.addOverlay({ + token: function(stream) { + for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {} + if (i > stream.pos) { stream.pos = i; return null; } + stream.pos = l; + return "trailingspace"; + }, + name: "trailingspace" + }); + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/fold/brace-fold.js b/public/vendor/plugins/codemirror/addon/fold/brace-fold.js new file mode 100644 index 0000000000..654d1fb691 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/fold/brace-fold.js @@ -0,0 +1,105 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerHelper("fold", "brace", function(cm, start) { + var line = start.line, lineText = cm.getLine(line); + var tokenType; + + function findOpening(openCh) { + for (var at = start.ch, pass = 0;;) { + var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1); + if (found == -1) { + if (pass == 1) break; + pass = 1; + at = lineText.length; + continue; + } + if (pass == 1 && found < start.ch) break; + tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)); + if (!/^(comment|string)/.test(tokenType)) return found + 1; + at = found - 1; + } + } + + var startToken = "{", endToken = "}", startCh = findOpening("{"); + if (startCh == null) { + startToken = "[", endToken = "]"; + startCh = findOpening("["); + } + + if (startCh == null) return; + var count = 1, lastLine = cm.lastLine(), end, endCh; + outer: for (var i = line; i <= lastLine; ++i) { + var text = cm.getLine(i), pos = i == line ? startCh : 0; + for (;;) { + var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); + if (nextOpen < 0) nextOpen = text.length; + if (nextClose < 0) nextClose = text.length; + pos = Math.min(nextOpen, nextClose); + if (pos == text.length) break; + if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) { + if (pos == nextOpen) ++count; + else if (!--count) { end = i; endCh = pos; break outer; } + } + ++pos; + } + } + if (end == null || line == end) return; + return {from: CodeMirror.Pos(line, startCh), + to: CodeMirror.Pos(end, endCh)}; +}); + +CodeMirror.registerHelper("fold", "import", function(cm, start) { + function hasImport(line) { + if (line < cm.firstLine() || line > cm.lastLine()) return null; + var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); + if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); + if (start.type != "keyword" || start.string != "import") return null; + // Now find closing semicolon, return its position + for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) { + var text = cm.getLine(i), semi = text.indexOf(";"); + if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)}; + } + } + + var startLine = start.line, has = hasImport(startLine), prev; + if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1)) + return null; + for (var end = has.end;;) { + var next = hasImport(end.line + 1); + if (next == null) break; + end = next.end; + } + return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end}; +}); + +CodeMirror.registerHelper("fold", "include", function(cm, start) { + function hasInclude(line) { + if (line < cm.firstLine() || line > cm.lastLine()) return null; + var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); + if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); + if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8; + } + + var startLine = start.line, has = hasInclude(startLine); + if (has == null || hasInclude(startLine - 1) != null) return null; + for (var end = startLine;;) { + var next = hasInclude(end + 1); + if (next == null) break; + ++end; + } + return {from: CodeMirror.Pos(startLine, has + 1), + to: cm.clipPos(CodeMirror.Pos(end))}; +}); + +}); diff --git a/public/vendor/plugins/codemirror/addon/fold/comment-fold.js b/public/vendor/plugins/codemirror/addon/fold/comment-fold.js new file mode 100644 index 0000000000..836101d8b0 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/fold/comment-fold.js @@ -0,0 +1,59 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerGlobalHelper("fold", "comment", function(mode) { + return mode.blockCommentStart && mode.blockCommentEnd; +}, function(cm, start) { + var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd; + if (!startToken || !endToken) return; + var line = start.line, lineText = cm.getLine(line); + + var startCh; + for (var at = start.ch, pass = 0;;) { + var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1); + if (found == -1) { + if (pass == 1) return; + pass = 1; + at = lineText.length; + continue; + } + if (pass == 1 && found < start.ch) return; + if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1))) && + (found == 0 || lineText.slice(found - endToken.length, found) == endToken || + !/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found))))) { + startCh = found + startToken.length; + break; + } + at = found - 1; + } + + var depth = 1, lastLine = cm.lastLine(), end, endCh; + outer: for (var i = line; i <= lastLine; ++i) { + var text = cm.getLine(i), pos = i == line ? startCh : 0; + for (;;) { + var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos); + if (nextOpen < 0) nextOpen = text.length; + if (nextClose < 0) nextClose = text.length; + pos = Math.min(nextOpen, nextClose); + if (pos == text.length) break; + if (pos == nextOpen) ++depth; + else if (!--depth) { end = i; endCh = pos; break outer; } + ++pos; + } + } + if (end == null || line == end && endCh == startCh) return; + return {from: CodeMirror.Pos(line, startCh), + to: CodeMirror.Pos(end, endCh)}; +}); + +}); diff --git a/public/vendor/plugins/codemirror/addon/fold/foldcode.js b/public/vendor/plugins/codemirror/addon/fold/foldcode.js new file mode 100644 index 0000000000..e146fb9f3e --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/fold/foldcode.js @@ -0,0 +1,152 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function doFold(cm, pos, options, force) { + if (options && options.call) { + var finder = options; + options = null; + } else { + var finder = getOption(cm, options, "rangeFinder"); + } + if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0); + var minSize = getOption(cm, options, "minFoldSize"); + + function getRange(allowFolded) { + var range = finder(cm, pos); + if (!range || range.to.line - range.from.line < minSize) return null; + var marks = cm.findMarksAt(range.from); + for (var i = 0; i < marks.length; ++i) { + if (marks[i].__isFold && force !== "fold") { + if (!allowFolded) return null; + range.cleared = true; + marks[i].clear(); + } + } + return range; + } + + var range = getRange(true); + if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) { + pos = CodeMirror.Pos(pos.line - 1, 0); + range = getRange(false); + } + if (!range || range.cleared || force === "unfold") return; + + var myWidget = makeWidget(cm, options); + CodeMirror.on(myWidget, "mousedown", function(e) { + myRange.clear(); + CodeMirror.e_preventDefault(e); + }); + var myRange = cm.markText(range.from, range.to, { + replacedWith: myWidget, + clearOnEnter: getOption(cm, options, "clearOnEnter"), + __isFold: true + }); + myRange.on("clear", function(from, to) { + CodeMirror.signal(cm, "unfold", cm, from, to); + }); + CodeMirror.signal(cm, "fold", cm, range.from, range.to); + } + + function makeWidget(cm, options) { + var widget = getOption(cm, options, "widget"); + if (typeof widget == "string") { + var text = document.createTextNode(widget); + widget = document.createElement("span"); + widget.appendChild(text); + widget.className = "CodeMirror-foldmarker"; + } else if (widget) { + widget = widget.cloneNode(true) + } + return widget; + } + + // Clumsy backwards-compatible interface + CodeMirror.newFoldFunction = function(rangeFinder, widget) { + return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); }; + }; + + // New-style interface + CodeMirror.defineExtension("foldCode", function(pos, options, force) { + doFold(this, pos, options, force); + }); + + CodeMirror.defineExtension("isFolded", function(pos) { + var marks = this.findMarksAt(pos); + for (var i = 0; i < marks.length; ++i) + if (marks[i].__isFold) return true; + }); + + CodeMirror.commands.toggleFold = function(cm) { + cm.foldCode(cm.getCursor()); + }; + CodeMirror.commands.fold = function(cm) { + cm.foldCode(cm.getCursor(), null, "fold"); + }; + CodeMirror.commands.unfold = function(cm) { + cm.foldCode(cm.getCursor(), null, "unfold"); + }; + CodeMirror.commands.foldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "fold"); + }); + }; + CodeMirror.commands.unfoldAll = function(cm) { + cm.operation(function() { + for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++) + cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold"); + }); + }; + + CodeMirror.registerHelper("fold", "combine", function() { + var funcs = Array.prototype.slice.call(arguments, 0); + return function(cm, start) { + for (var i = 0; i < funcs.length; ++i) { + var found = funcs[i](cm, start); + if (found) return found; + } + }; + }); + + CodeMirror.registerHelper("fold", "auto", function(cm, start) { + var helpers = cm.getHelpers(start, "fold"); + for (var i = 0; i < helpers.length; i++) { + var cur = helpers[i](cm, start); + if (cur) return cur; + } + }); + + var defaultOptions = { + rangeFinder: CodeMirror.fold.auto, + widget: "\u2194", + minFoldSize: 0, + scanUp: false, + clearOnEnter: true + }; + + CodeMirror.defineOption("foldOptions", null); + + function getOption(cm, options, name) { + if (options && options[name] !== undefined) + return options[name]; + var editorOptions = cm.options.foldOptions; + if (editorOptions && editorOptions[name] !== undefined) + return editorOptions[name]; + return defaultOptions[name]; + } + + CodeMirror.defineExtension("foldOption", function(options, name) { + return getOption(this, options, name); + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/fold/foldgutter.css b/public/vendor/plugins/codemirror/addon/fold/foldgutter.css new file mode 100644 index 0000000000..ad19ae2d3e --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/fold/foldgutter.css @@ -0,0 +1,20 @@ +.CodeMirror-foldmarker { + color: blue; + text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; + font-family: arial; + line-height: .3; + cursor: pointer; +} +.CodeMirror-foldgutter { + width: .7em; +} +.CodeMirror-foldgutter-open, +.CodeMirror-foldgutter-folded { + cursor: pointer; +} +.CodeMirror-foldgutter-open:after { + content: "\25BE"; +} +.CodeMirror-foldgutter-folded:after { + content: "\25B8"; +} diff --git a/public/vendor/plugins/codemirror/addon/fold/foldgutter.js b/public/vendor/plugins/codemirror/addon/fold/foldgutter.js new file mode 100644 index 0000000000..e57a1df35d --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/fold/foldgutter.js @@ -0,0 +1,151 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./foldcode")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./foldcode"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("foldGutter", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.clearGutter(cm.state.foldGutter.options.gutter); + cm.state.foldGutter = null; + cm.off("gutterClick", onGutterClick); + cm.off("changes", onChange); + cm.off("viewportChange", onViewportChange); + cm.off("fold", onFold); + cm.off("unfold", onFold); + cm.off("swapDoc", onChange); + } + if (val) { + cm.state.foldGutter = new State(parseOptions(val)); + updateInViewport(cm); + cm.on("gutterClick", onGutterClick); + cm.on("changes", onChange); + cm.on("viewportChange", onViewportChange); + cm.on("fold", onFold); + cm.on("unfold", onFold); + cm.on("swapDoc", onChange); + } + }); + + var Pos = CodeMirror.Pos; + + function State(options) { + this.options = options; + this.from = this.to = 0; + } + + function parseOptions(opts) { + if (opts === true) opts = {}; + if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter"; + if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open"; + if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded"; + return opts; + } + + function isFolded(cm, line) { + var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0)); + for (var i = 0; i < marks.length; ++i) { + if (marks[i].__isFold) { + var fromPos = marks[i].find(-1); + if (fromPos && fromPos.line === line) + return marks[i]; + } + } + } + + function marker(spec) { + if (typeof spec == "string") { + var elt = document.createElement("div"); + elt.className = spec + " CodeMirror-guttermarker-subtle"; + return elt; + } else { + return spec.cloneNode(true); + } + } + + function updateFoldInfo(cm, from, to) { + var opts = cm.state.foldGutter.options, cur = from; + var minSize = cm.foldOption(opts, "minFoldSize"); + var func = cm.foldOption(opts, "rangeFinder"); + cm.eachLine(from, to, function(line) { + var mark = null; + if (isFolded(cm, cur)) { + mark = marker(opts.indicatorFolded); + } else { + var pos = Pos(cur, 0); + var range = func && func(cm, pos); + if (range && range.to.line - range.from.line >= minSize) + mark = marker(opts.indicatorOpen); + } + cm.setGutterMarker(line, opts.gutter, mark); + ++cur; + }); + } + + function updateInViewport(cm) { + var vp = cm.getViewport(), state = cm.state.foldGutter; + if (!state) return; + cm.operation(function() { + updateFoldInfo(cm, vp.from, vp.to); + }); + state.from = vp.from; state.to = vp.to; + } + + function onGutterClick(cm, line, gutter) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + if (gutter != opts.gutter) return; + var folded = isFolded(cm, line); + if (folded) folded.clear(); + else cm.foldCode(Pos(line, 0), opts); + } + + function onChange(cm) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + state.from = state.to = 0; + clearTimeout(state.changeUpdate); + state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600); + } + + function onViewportChange(cm) { + var state = cm.state.foldGutter; + if (!state) return; + var opts = state.options; + clearTimeout(state.changeUpdate); + state.changeUpdate = setTimeout(function() { + var vp = cm.getViewport(); + if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { + updateInViewport(cm); + } else { + cm.operation(function() { + if (vp.from < state.from) { + updateFoldInfo(cm, vp.from, state.from); + state.from = vp.from; + } + if (vp.to > state.to) { + updateFoldInfo(cm, state.to, vp.to); + state.to = vp.to; + } + }); + } + }, opts.updateViewportTimeSpan || 400); + } + + function onFold(cm, from) { + var state = cm.state.foldGutter; + if (!state) return; + var line = from.line; + if (line >= state.from && line < state.to) + updateFoldInfo(cm, line, line + 1); + } +}); diff --git a/public/vendor/plugins/codemirror/addon/fold/indent-fold.js b/public/vendor/plugins/codemirror/addon/fold/indent-fold.js new file mode 100644 index 0000000000..0cc1126440 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/fold/indent-fold.js @@ -0,0 +1,48 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +function lineIndent(cm, lineNo) { + var text = cm.getLine(lineNo) + var spaceTo = text.search(/\S/) + if (spaceTo == -1 || /\bcomment\b/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, spaceTo + 1)))) + return -1 + return CodeMirror.countColumn(text, null, cm.getOption("tabSize")) +} + +CodeMirror.registerHelper("fold", "indent", function(cm, start) { + var myIndent = lineIndent(cm, start.line) + if (myIndent < 0) return + var lastLineInFold = null + + // Go through lines until we find a line that definitely doesn't belong in + // the block we're folding, or to the end. + for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) { + var indent = lineIndent(cm, i) + if (indent == -1) { + } else if (indent > myIndent) { + // Lines with a greater indent are considered part of the block. + lastLineInFold = i; + } else { + // If this line has non-space, non-comment content, and is + // indented less or equal to the start line, it is the start of + // another block. + break; + } + } + if (lastLineInFold) return { + from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), + to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length) + }; +}); + +}); diff --git a/public/vendor/plugins/codemirror/addon/fold/markdown-fold.js b/public/vendor/plugins/codemirror/addon/fold/markdown-fold.js new file mode 100644 index 0000000000..6a551786d1 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/fold/markdown-fold.js @@ -0,0 +1,49 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerHelper("fold", "markdown", function(cm, start) { + var maxDepth = 100; + + function isHeader(lineNo) { + var tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)); + return tokentype && /\bheader\b/.test(tokentype); + } + + function headerLevel(lineNo, line, nextLine) { + var match = line && line.match(/^#+/); + if (match && isHeader(lineNo)) return match[0].length; + match = nextLine && nextLine.match(/^[=\-]+\s*$/); + if (match && isHeader(lineNo + 1)) return nextLine[0] == "=" ? 1 : 2; + return maxDepth; + } + + var firstLine = cm.getLine(start.line), nextLine = cm.getLine(start.line + 1); + var level = headerLevel(start.line, firstLine, nextLine); + if (level === maxDepth) return undefined; + + var lastLineNo = cm.lastLine(); + var end = start.line, nextNextLine = cm.getLine(end + 2); + while (end < lastLineNo) { + if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break; + ++end; + nextLine = nextNextLine; + nextNextLine = cm.getLine(end + 2); + } + + return { + from: CodeMirror.Pos(start.line, firstLine.length), + to: CodeMirror.Pos(end, cm.getLine(end).length) + }; +}); + +}); diff --git a/public/vendor/plugins/codemirror/addon/fold/xml-fold.js b/public/vendor/plugins/codemirror/addon/fold/xml-fold.js new file mode 100644 index 0000000000..13bc3838b2 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/fold/xml-fold.js @@ -0,0 +1,184 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var Pos = CodeMirror.Pos; + function cmp(a, b) { return a.line - b.line || a.ch - b.ch; } + + var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; + var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; + var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g"); + + function Iter(cm, line, ch, range) { + this.line = line; this.ch = ch; + this.cm = cm; this.text = cm.getLine(line); + this.min = range ? Math.max(range.from, cm.firstLine()) : cm.firstLine(); + this.max = range ? Math.min(range.to - 1, cm.lastLine()) : cm.lastLine(); + } + + function tagAt(iter, ch) { + var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch)); + return type && /\btag\b/.test(type); + } + + function nextLine(iter) { + if (iter.line >= iter.max) return; + iter.ch = 0; + iter.text = iter.cm.getLine(++iter.line); + return true; + } + function prevLine(iter) { + if (iter.line <= iter.min) return; + iter.text = iter.cm.getLine(--iter.line); + iter.ch = iter.text.length; + return true; + } + + function toTagEnd(iter) { + for (;;) { + var gt = iter.text.indexOf(">", iter.ch); + if (gt == -1) { if (nextLine(iter)) continue; else return; } + if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; } + var lastSlash = iter.text.lastIndexOf("/", gt); + var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); + iter.ch = gt + 1; + return selfClose ? "selfClose" : "regular"; + } + } + function toTagStart(iter) { + for (;;) { + var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1; + if (lt == -1) { if (prevLine(iter)) continue; else return; } + if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; } + xmlTagStart.lastIndex = lt; + iter.ch = lt; + var match = xmlTagStart.exec(iter.text); + if (match && match.index == lt) return match; + } + } + + function toNextTag(iter) { + for (;;) { + xmlTagStart.lastIndex = iter.ch; + var found = xmlTagStart.exec(iter.text); + if (!found) { if (nextLine(iter)) continue; else return; } + if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; } + iter.ch = found.index + found[0].length; + return found; + } + } + function toPrevTag(iter) { + for (;;) { + var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1; + if (gt == -1) { if (prevLine(iter)) continue; else return; } + if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; } + var lastSlash = iter.text.lastIndexOf("/", gt); + var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt)); + iter.ch = gt + 1; + return selfClose ? "selfClose" : "regular"; + } + } + + function findMatchingClose(iter, tag) { + var stack = []; + for (;;) { + var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0); + if (!next || !(end = toTagEnd(iter))) return; + if (end == "selfClose") continue; + if (next[1]) { // closing tag + for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) { + stack.length = i; + break; + } + if (i < 0 && (!tag || tag == next[2])) return { + tag: next[2], + from: Pos(startLine, startCh), + to: Pos(iter.line, iter.ch) + }; + } else { // opening tag + stack.push(next[2]); + } + } + } + function findMatchingOpen(iter, tag) { + var stack = []; + for (;;) { + var prev = toPrevTag(iter); + if (!prev) return; + if (prev == "selfClose") { toTagStart(iter); continue; } + var endLine = iter.line, endCh = iter.ch; + var start = toTagStart(iter); + if (!start) return; + if (start[1]) { // closing tag + stack.push(start[2]); + } else { // opening tag + for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) { + stack.length = i; + break; + } + if (i < 0 && (!tag || tag == start[2])) return { + tag: start[2], + from: Pos(iter.line, iter.ch), + to: Pos(endLine, endCh) + }; + } + } + } + + CodeMirror.registerHelper("fold", "xml", function(cm, start) { + var iter = new Iter(cm, start.line, 0); + for (;;) { + var openTag = toNextTag(iter) + if (!openTag || iter.line != start.line) return + var end = toTagEnd(iter) + if (!end) return + if (!openTag[1] && end != "selfClose") { + var startPos = Pos(iter.line, iter.ch); + var endPos = findMatchingClose(iter, openTag[2]); + return endPos && cmp(endPos.from, startPos) > 0 ? {from: startPos, to: endPos.from} : null + } + } + }); + CodeMirror.findMatchingTag = function(cm, pos, range) { + var iter = new Iter(cm, pos.line, pos.ch, range); + if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return; + var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch); + var start = end && toTagStart(iter); + if (!end || !start || cmp(iter, pos) > 0) return; + var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]}; + if (end == "selfClose") return {open: here, close: null, at: "open"}; + + if (start[1]) { // closing tag + return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"}; + } else { // opening tag + iter = new Iter(cm, to.line, to.ch, range); + return {open: here, close: findMatchingClose(iter, start[2]), at: "open"}; + } + }; + + CodeMirror.findEnclosingTag = function(cm, pos, range, tag) { + var iter = new Iter(cm, pos.line, pos.ch, range); + for (;;) { + var open = findMatchingOpen(iter, tag); + if (!open) break; + var forward = new Iter(cm, pos.line, pos.ch, range); + var close = findMatchingClose(forward, open.tag); + if (close) return {open: open, close: close}; + } + }; + + // Used by addon/edit/closetag.js + CodeMirror.scanForClosingTag = function(cm, pos, name, end) { + var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null); + return findMatchingClose(iter, name); + }; +}); diff --git a/public/vendor/plugins/codemirror/addon/hint/anyword-hint.js b/public/vendor/plugins/codemirror/addon/hint/anyword-hint.js new file mode 100644 index 0000000000..d27a9ec018 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/hint/anyword-hint.js @@ -0,0 +1,41 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var WORD = /[\w$]+/, RANGE = 500; + + CodeMirror.registerHelper("hint", "anyword", function(editor, options) { + var word = options && options.word || WORD; + var range = options && options.range || RANGE; + var cur = editor.getCursor(), curLine = editor.getLine(cur.line); + var end = cur.ch, start = end; + while (start && word.test(curLine.charAt(start - 1))) --start; + var curWord = start != end && curLine.slice(start, end); + + var list = options && options.list || [], seen = {}; + var re = new RegExp(word.source, "g"); + for (var dir = -1; dir <= 1; dir += 2) { + var line = cur.line, endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir; + for (; line != endLine; line += dir) { + var text = editor.getLine(line), m; + while (m = re.exec(text)) { + if (line == cur.line && m[0] === curWord) continue; + if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) { + seen[m[0]] = true; + list.push(m[0]); + } + } + } + } + return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)}; + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/hint/css-hint.js b/public/vendor/plugins/codemirror/addon/hint/css-hint.js new file mode 100644 index 0000000000..6cdf728195 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/hint/css-hint.js @@ -0,0 +1,60 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../../mode/css/css")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../../mode/css/css"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var pseudoClasses = {link: 1, visited: 1, active: 1, hover: 1, focus: 1, + "first-letter": 1, "first-line": 1, "first-child": 1, + before: 1, after: 1, lang: 1}; + + CodeMirror.registerHelper("hint", "css", function(cm) { + var cur = cm.getCursor(), token = cm.getTokenAt(cur); + var inner = CodeMirror.innerMode(cm.getMode(), token.state); + if (inner.mode.name != "css") return; + + if (token.type == "keyword" && "!important".indexOf(token.string) == 0) + return {list: ["!important"], from: CodeMirror.Pos(cur.line, token.start), + to: CodeMirror.Pos(cur.line, token.end)}; + + var start = token.start, end = cur.ch, word = token.string.slice(0, end - start); + if (/[^\w$_-]/.test(word)) { + word = ""; start = end = cur.ch; + } + + var spec = CodeMirror.resolveMode("text/css"); + + var result = []; + function add(keywords) { + for (var name in keywords) + if (!word || name.lastIndexOf(word, 0) == 0) + result.push(name); + } + + var st = inner.state.state; + if (st == "pseudo" || token.type == "variable-3") { + add(pseudoClasses); + } else if (st == "block" || st == "maybeprop") { + add(spec.propertyKeywords); + } else if (st == "prop" || st == "parens" || st == "at" || st == "params") { + add(spec.valueKeywords); + add(spec.colorKeywords); + } else if (st == "media" || st == "media_parens") { + add(spec.mediaTypes); + add(spec.mediaFeatures); + } + + if (result.length) return { + list: result, + from: CodeMirror.Pos(cur.line, start), + to: CodeMirror.Pos(cur.line, end) + }; + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/hint/html-hint.js b/public/vendor/plugins/codemirror/addon/hint/html-hint.js new file mode 100644 index 0000000000..d0cca4f6a2 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/hint/html-hint.js @@ -0,0 +1,350 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./xml-hint")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./xml-hint"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var langs = "ab aa af ak sq am ar an hy as av ae ay az bm ba eu be bn bh bi bs br bg my ca ch ce ny zh cv kw co cr hr cs da dv nl dz en eo et ee fo fj fi fr ff gl ka de el gn gu ht ha he hz hi ho hu ia id ie ga ig ik io is it iu ja jv kl kn kr ks kk km ki rw ky kv kg ko ku kj la lb lg li ln lo lt lu lv gv mk mg ms ml mt mi mr mh mn na nv nb nd ne ng nn no ii nr oc oj cu om or os pa pi fa pl ps pt qu rm rn ro ru sa sc sd se sm sg sr gd sn si sk sl so st es su sw ss sv ta te tg th ti bo tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa cy wo fy xh yi yo za zu".split(" "); + var targets = ["_blank", "_self", "_top", "_parent"]; + var charsets = ["ascii", "utf-8", "utf-16", "latin1", "latin1"]; + var methods = ["get", "post", "put", "delete"]; + var encs = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]; + var media = ["all", "screen", "print", "embossed", "braille", "handheld", "print", "projection", "screen", "tty", "tv", "speech", + "3d-glasses", "resolution [>][<][=] [X]", "device-aspect-ratio: X/Y", "orientation:portrait", + "orientation:landscape", "device-height: [X]", "device-width: [X]"]; + var s = { attrs: {} }; // Simple tag, reused for a whole lot of tags + + var data = { + a: { + attrs: { + href: null, ping: null, type: null, + media: media, + target: targets, + hreflang: langs + } + }, + abbr: s, + acronym: s, + address: s, + applet: s, + area: { + attrs: { + alt: null, coords: null, href: null, target: null, ping: null, + media: media, hreflang: langs, type: null, + shape: ["default", "rect", "circle", "poly"] + } + }, + article: s, + aside: s, + audio: { + attrs: { + src: null, mediagroup: null, + crossorigin: ["anonymous", "use-credentials"], + preload: ["none", "metadata", "auto"], + autoplay: ["", "autoplay"], + loop: ["", "loop"], + controls: ["", "controls"] + } + }, + b: s, + base: { attrs: { href: null, target: targets } }, + basefont: s, + bdi: s, + bdo: s, + big: s, + blockquote: { attrs: { cite: null } }, + body: s, + br: s, + button: { + attrs: { + form: null, formaction: null, name: null, value: null, + autofocus: ["", "autofocus"], + disabled: ["", "autofocus"], + formenctype: encs, + formmethod: methods, + formnovalidate: ["", "novalidate"], + formtarget: targets, + type: ["submit", "reset", "button"] + } + }, + canvas: { attrs: { width: null, height: null } }, + caption: s, + center: s, + cite: s, + code: s, + col: { attrs: { span: null } }, + colgroup: { attrs: { span: null } }, + command: { + attrs: { + type: ["command", "checkbox", "radio"], + label: null, icon: null, radiogroup: null, command: null, title: null, + disabled: ["", "disabled"], + checked: ["", "checked"] + } + }, + data: { attrs: { value: null } }, + datagrid: { attrs: { disabled: ["", "disabled"], multiple: ["", "multiple"] } }, + datalist: { attrs: { data: null } }, + dd: s, + del: { attrs: { cite: null, datetime: null } }, + details: { attrs: { open: ["", "open"] } }, + dfn: s, + dir: s, + div: s, + dl: s, + dt: s, + em: s, + embed: { attrs: { src: null, type: null, width: null, height: null } }, + eventsource: { attrs: { src: null } }, + fieldset: { attrs: { disabled: ["", "disabled"], form: null, name: null } }, + figcaption: s, + figure: s, + font: s, + footer: s, + form: { + attrs: { + action: null, name: null, + "accept-charset": charsets, + autocomplete: ["on", "off"], + enctype: encs, + method: methods, + novalidate: ["", "novalidate"], + target: targets + } + }, + frame: s, + frameset: s, + h1: s, h2: s, h3: s, h4: s, h5: s, h6: s, + head: { + attrs: {}, + children: ["title", "base", "link", "style", "meta", "script", "noscript", "command"] + }, + header: s, + hgroup: s, + hr: s, + html: { + attrs: { manifest: null }, + children: ["head", "body"] + }, + i: s, + iframe: { + attrs: { + src: null, srcdoc: null, name: null, width: null, height: null, + sandbox: ["allow-top-navigation", "allow-same-origin", "allow-forms", "allow-scripts"], + seamless: ["", "seamless"] + } + }, + img: { + attrs: { + alt: null, src: null, ismap: null, usemap: null, width: null, height: null, + crossorigin: ["anonymous", "use-credentials"] + } + }, + input: { + attrs: { + alt: null, dirname: null, form: null, formaction: null, + height: null, list: null, max: null, maxlength: null, min: null, + name: null, pattern: null, placeholder: null, size: null, src: null, + step: null, value: null, width: null, + accept: ["audio/*", "video/*", "image/*"], + autocomplete: ["on", "off"], + autofocus: ["", "autofocus"], + checked: ["", "checked"], + disabled: ["", "disabled"], + formenctype: encs, + formmethod: methods, + formnovalidate: ["", "novalidate"], + formtarget: targets, + multiple: ["", "multiple"], + readonly: ["", "readonly"], + required: ["", "required"], + type: ["hidden", "text", "search", "tel", "url", "email", "password", "datetime", "date", "month", + "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", + "file", "submit", "image", "reset", "button"] + } + }, + ins: { attrs: { cite: null, datetime: null } }, + kbd: s, + keygen: { + attrs: { + challenge: null, form: null, name: null, + autofocus: ["", "autofocus"], + disabled: ["", "disabled"], + keytype: ["RSA"] + } + }, + label: { attrs: { "for": null, form: null } }, + legend: s, + li: { attrs: { value: null } }, + link: { + attrs: { + href: null, type: null, + hreflang: langs, + media: media, + sizes: ["all", "16x16", "16x16 32x32", "16x16 32x32 64x64"] + } + }, + map: { attrs: { name: null } }, + mark: s, + menu: { attrs: { label: null, type: ["list", "context", "toolbar"] } }, + meta: { + attrs: { + content: null, + charset: charsets, + name: ["viewport", "application-name", "author", "description", "generator", "keywords"], + "http-equiv": ["content-language", "content-type", "default-style", "refresh"] + } + }, + meter: { attrs: { value: null, min: null, low: null, high: null, max: null, optimum: null } }, + nav: s, + noframes: s, + noscript: s, + object: { + attrs: { + data: null, type: null, name: null, usemap: null, form: null, width: null, height: null, + typemustmatch: ["", "typemustmatch"] + } + }, + ol: { attrs: { reversed: ["", "reversed"], start: null, type: ["1", "a", "A", "i", "I"] } }, + optgroup: { attrs: { disabled: ["", "disabled"], label: null } }, + option: { attrs: { disabled: ["", "disabled"], label: null, selected: ["", "selected"], value: null } }, + output: { attrs: { "for": null, form: null, name: null } }, + p: s, + param: { attrs: { name: null, value: null } }, + pre: s, + progress: { attrs: { value: null, max: null } }, + q: { attrs: { cite: null } }, + rp: s, + rt: s, + ruby: s, + s: s, + samp: s, + script: { + attrs: { + type: ["text/javascript"], + src: null, + async: ["", "async"], + defer: ["", "defer"], + charset: charsets + } + }, + section: s, + select: { + attrs: { + form: null, name: null, size: null, + autofocus: ["", "autofocus"], + disabled: ["", "disabled"], + multiple: ["", "multiple"] + } + }, + small: s, + source: { attrs: { src: null, type: null, media: null } }, + span: s, + strike: s, + strong: s, + style: { + attrs: { + type: ["text/css"], + media: media, + scoped: null + } + }, + sub: s, + summary: s, + sup: s, + table: s, + tbody: s, + td: { attrs: { colspan: null, rowspan: null, headers: null } }, + textarea: { + attrs: { + dirname: null, form: null, maxlength: null, name: null, placeholder: null, + rows: null, cols: null, + autofocus: ["", "autofocus"], + disabled: ["", "disabled"], + readonly: ["", "readonly"], + required: ["", "required"], + wrap: ["soft", "hard"] + } + }, + tfoot: s, + th: { attrs: { colspan: null, rowspan: null, headers: null, scope: ["row", "col", "rowgroup", "colgroup"] } }, + thead: s, + time: { attrs: { datetime: null } }, + title: s, + tr: s, + track: { + attrs: { + src: null, label: null, "default": null, + kind: ["subtitles", "captions", "descriptions", "chapters", "metadata"], + srclang: langs + } + }, + tt: s, + u: s, + ul: s, + "var": s, + video: { + attrs: { + src: null, poster: null, width: null, height: null, + crossorigin: ["anonymous", "use-credentials"], + preload: ["auto", "metadata", "none"], + autoplay: ["", "autoplay"], + mediagroup: ["movie"], + muted: ["", "muted"], + controls: ["", "controls"] + } + }, + wbr: s + }; + + var globalAttrs = { + accesskey: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + "class": null, + contenteditable: ["true", "false"], + contextmenu: null, + dir: ["ltr", "rtl", "auto"], + draggable: ["true", "false", "auto"], + dropzone: ["copy", "move", "link", "string:", "file:"], + hidden: ["hidden"], + id: null, + inert: ["inert"], + itemid: null, + itemprop: null, + itemref: null, + itemscope: ["itemscope"], + itemtype: null, + lang: ["en", "es"], + spellcheck: ["true", "false"], + autocorrect: ["true", "false"], + autocapitalize: ["true", "false"], + style: null, + tabindex: ["1", "2", "3", "4", "5", "6", "7", "8", "9"], + title: null, + translate: ["yes", "no"], + onclick: null, + rel: ["stylesheet", "alternate", "author", "bookmark", "help", "license", "next", "nofollow", "noreferrer", "prefetch", "prev", "search", "tag"] + }; + function populate(obj) { + for (var attr in globalAttrs) if (globalAttrs.hasOwnProperty(attr)) + obj.attrs[attr] = globalAttrs[attr]; + } + + populate(s); + for (var tag in data) if (data.hasOwnProperty(tag) && data[tag] != s) + populate(data[tag]); + + CodeMirror.htmlSchema = data; + function htmlHint(cm, options) { + var local = {schemaInfo: data}; + if (options) for (var opt in options) local[opt] = options[opt]; + return CodeMirror.hint.xml(cm, local); + } + CodeMirror.registerHelper("hint", "html", htmlHint); +}); diff --git a/public/vendor/plugins/codemirror/addon/hint/javascript-hint.js b/public/vendor/plugins/codemirror/addon/hint/javascript-hint.js new file mode 100644 index 0000000000..96a7fe01c2 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/hint/javascript-hint.js @@ -0,0 +1,157 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var Pos = CodeMirror.Pos; + + function forEach(arr, f) { + for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); + } + + function arrayContains(arr, item) { + if (!Array.prototype.indexOf) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + return true; + } + } + return false; + } + return arr.indexOf(item) != -1; + } + + function scriptHint(editor, keywords, getToken, options) { + // Find the token at the cursor + var cur = editor.getCursor(), token = getToken(editor, cur); + if (/\b(?:string|comment)\b/.test(token.type)) return; + var innerMode = CodeMirror.innerMode(editor.getMode(), token.state); + if (innerMode.mode.helperType === "json") return; + token.state = innerMode.state; + + // If it's not a 'word-style' token, ignore the token. + if (!/^[\w$_]*$/.test(token.string)) { + token = {start: cur.ch, end: cur.ch, string: "", state: token.state, + type: token.string == "." ? "property" : null}; + } else if (token.end > cur.ch) { + token.end = cur.ch; + token.string = token.string.slice(0, cur.ch - token.start); + } + + var tprop = token; + // If it is a property, find out what it is a property of. + while (tprop.type == "property") { + tprop = getToken(editor, Pos(cur.line, tprop.start)); + if (tprop.string != ".") return; + tprop = getToken(editor, Pos(cur.line, tprop.start)); + if (!context) var context = []; + context.push(tprop); + } + return {list: getCompletions(token, context, keywords, options), + from: Pos(cur.line, token.start), + to: Pos(cur.line, token.end)}; + } + + function javascriptHint(editor, options) { + return scriptHint(editor, javascriptKeywords, + function (e, cur) {return e.getTokenAt(cur);}, + options); + }; + CodeMirror.registerHelper("hint", "javascript", javascriptHint); + + function getCoffeeScriptToken(editor, cur) { + // This getToken, it is for coffeescript, imitates the behavior of + // getTokenAt method in javascript.js, that is, returning "property" + // type and treat "." as indepenent token. + var token = editor.getTokenAt(cur); + if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') { + token.end = token.start; + token.string = '.'; + token.type = "property"; + } + else if (/^\.[\w$_]*$/.test(token.string)) { + token.type = "property"; + token.start++; + token.string = token.string.replace(/\./, ''); + } + return token; + } + + function coffeescriptHint(editor, options) { + return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options); + } + CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint); + + var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " + + "toUpperCase toLowerCase split concat match replace search").split(" "); + var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " + + "lastIndexOf every some filter forEach map reduce reduceRight ").split(" "); + var funcProps = "prototype apply call bind".split(" "); + var javascriptKeywords = ("break case catch class const continue debugger default delete do else export extends false finally for function " + + "if in import instanceof new null return super switch this throw true try typeof var void while with yield").split(" "); + var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " + + "if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" "); + + function forAllProps(obj, callback) { + if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) { + for (var name in obj) callback(name) + } else { + for (var o = obj; o; o = Object.getPrototypeOf(o)) + Object.getOwnPropertyNames(o).forEach(callback) + } + } + + function getCompletions(token, context, keywords, options) { + var found = [], start = token.string, global = options && options.globalScope || window; + function maybeAdd(str) { + if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str); + } + function gatherCompletions(obj) { + if (typeof obj == "string") forEach(stringProps, maybeAdd); + else if (obj instanceof Array) forEach(arrayProps, maybeAdd); + else if (obj instanceof Function) forEach(funcProps, maybeAdd); + forAllProps(obj, maybeAdd) + } + + if (context && context.length) { + // If this is a property, see if it belongs to some object we can + // find in the current environment. + var obj = context.pop(), base; + if (obj.type && obj.type.indexOf("variable") === 0) { + if (options && options.additionalContext) + base = options.additionalContext[obj.string]; + if (!options || options.useGlobalScope !== false) + base = base || global[obj.string]; + } else if (obj.type == "string") { + base = ""; + } else if (obj.type == "atom") { + base = 1; + } else if (obj.type == "function") { + if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') && + (typeof global.jQuery == 'function')) + base = global.jQuery(); + else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function')) + base = global._(); + } + while (base != null && context.length) + base = base[context.pop().string]; + if (base != null) gatherCompletions(base); + } else { + // If not, just look in the global object and any local scope + // (reading into JS mode internals to get at the local and global variables) + for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name); + for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name); + if (!options || options.useGlobalScope !== false) + gatherCompletions(global); + forEach(keywords, maybeAdd); + } + return found; + } +}); diff --git a/public/vendor/plugins/codemirror/addon/hint/show-hint.css b/public/vendor/plugins/codemirror/addon/hint/show-hint.css new file mode 100644 index 0000000000..5617ccca2b --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/hint/show-hint.css @@ -0,0 +1,36 @@ +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + padding: 2px; + + -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + box-shadow: 2px 3px 5px rgba(0,0,0,.2); + border-radius: 3px; + border: 1px solid silver; + + background: white; + font-size: 90%; + font-family: monospace; + + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + white-space: pre; + color: black; + cursor: pointer; +} + +li.CodeMirror-hint-active { + background: #08f; + color: white; +} diff --git a/public/vendor/plugins/codemirror/addon/hint/show-hint.js b/public/vendor/plugins/codemirror/addon/hint/show-hint.js new file mode 100644 index 0000000000..d70b2ab173 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/hint/show-hint.js @@ -0,0 +1,460 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var HINT_ELEMENT_CLASS = "CodeMirror-hint"; + var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; + + // This is the old interface, kept around for now to stay + // backwards-compatible. + CodeMirror.showHint = function(cm, getHints, options) { + if (!getHints) return cm.showHint(options); + if (options && options.async) getHints.async = true; + var newOpts = {hint: getHints}; + if (options) for (var prop in options) newOpts[prop] = options[prop]; + return cm.showHint(newOpts); + }; + + CodeMirror.defineExtension("showHint", function(options) { + options = parseOptions(this, this.getCursor("start"), options); + var selections = this.listSelections() + if (selections.length > 1) return; + // By default, don't allow completion when something is selected. + // A hint function can have a `supportsSelection` property to + // indicate that it can handle selections. + if (this.somethingSelected()) { + if (!options.hint.supportsSelection) return; + // Don't try with cross-line selections + for (var i = 0; i < selections.length; i++) + if (selections[i].head.line != selections[i].anchor.line) return; + } + + if (this.state.completionActive) this.state.completionActive.close(); + var completion = this.state.completionActive = new Completion(this, options); + if (!completion.options.hint) return; + + CodeMirror.signal(this, "startCompletion", this); + completion.update(true); + }); + + CodeMirror.defineExtension("closeHint", function() { + if (this.state.completionActive) this.state.completionActive.close() + }) + + function Completion(cm, options) { + this.cm = cm; + this.options = options; + this.widget = null; + this.debounce = 0; + this.tick = 0; + this.startPos = this.cm.getCursor("start"); + this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; + + var self = this; + cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); }); + } + + var requestAnimationFrame = window.requestAnimationFrame || function(fn) { + return setTimeout(fn, 1000/60); + }; + var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; + + Completion.prototype = { + close: function() { + if (!this.active()) return; + this.cm.state.completionActive = null; + this.tick = null; + this.cm.off("cursorActivity", this.activityFunc); + + if (this.widget && this.data) CodeMirror.signal(this.data, "close"); + if (this.widget) this.widget.close(); + CodeMirror.signal(this.cm, "endCompletion", this.cm); + }, + + active: function() { + return this.cm.state.completionActive == this; + }, + + pick: function(data, i) { + var completion = data.list[i]; + if (completion.hint) completion.hint(this.cm, data, completion); + else this.cm.replaceRange(getText(completion), completion.from || data.from, + completion.to || data.to, "complete"); + CodeMirror.signal(data, "pick", completion); + this.close(); + }, + + cursorActivity: function() { + if (this.debounce) { + cancelAnimationFrame(this.debounce); + this.debounce = 0; + } + + var pos = this.cm.getCursor(), line = this.cm.getLine(pos.line); + if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || + pos.ch < this.startPos.ch || this.cm.somethingSelected() || + (!pos.ch || this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { + this.close(); + } else { + var self = this; + this.debounce = requestAnimationFrame(function() {self.update();}); + if (this.widget) this.widget.disable(); + } + }, + + update: function(first) { + if (this.tick == null) return + var self = this, myTick = ++this.tick + fetchHints(this.options.hint, this.cm, this.options, function(data) { + if (self.tick == myTick) self.finishUpdate(data, first) + }) + }, + + finishUpdate: function(data, first) { + if (this.data) CodeMirror.signal(this.data, "update"); + + var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); + if (this.widget) this.widget.close(); + + this.data = data; + + if (data && data.list.length) { + if (picked && data.list.length == 1) { + this.pick(data, 0); + } else { + this.widget = new Widget(this, data); + CodeMirror.signal(data, "shown"); + } + } + } + }; + + function parseOptions(cm, pos, options) { + var editor = cm.options.hintOptions; + var out = {}; + for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; + if (editor) for (var prop in editor) + if (editor[prop] !== undefined) out[prop] = editor[prop]; + if (options) for (var prop in options) + if (options[prop] !== undefined) out[prop] = options[prop]; + if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos) + return out; + } + + function getText(completion) { + if (typeof completion == "string") return completion; + else return completion.text; + } + + function buildKeyMap(completion, handle) { + var baseMap = { + Up: function() {handle.moveFocus(-1);}, + Down: function() {handle.moveFocus(1);}, + PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, + PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, + Home: function() {handle.setFocus(0);}, + End: function() {handle.setFocus(handle.length - 1);}, + Enter: handle.pick, + Tab: handle.pick, + Esc: handle.close + }; + + var mac = /Mac/.test(navigator.platform); + + if (mac) { + baseMap["Ctrl-P"] = function() {handle.moveFocus(-1);}; + baseMap["Ctrl-N"] = function() {handle.moveFocus(1);}; + } + + var custom = completion.options.customKeys; + var ourMap = custom ? {} : baseMap; + function addBinding(key, val) { + var bound; + if (typeof val != "string") + bound = function(cm) { return val(cm, handle); }; + // This mechanism is deprecated + else if (baseMap.hasOwnProperty(val)) + bound = baseMap[val]; + else + bound = val; + ourMap[key] = bound; + } + if (custom) + for (var key in custom) if (custom.hasOwnProperty(key)) + addBinding(key, custom[key]); + var extra = completion.options.extraKeys; + if (extra) + for (var key in extra) if (extra.hasOwnProperty(key)) + addBinding(key, extra[key]); + return ourMap; + } + + function getHintElement(hintsElement, el) { + while (el && el != hintsElement) { + if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; + el = el.parentNode; + } + } + + function Widget(completion, data) { + this.completion = completion; + this.data = data; + this.picked = false; + var widget = this, cm = completion.cm; + var ownerDocument = cm.getInputField().ownerDocument; + var parentWindow = ownerDocument.defaultView || ownerDocument.parentWindow; + + var hints = this.hints = ownerDocument.createElement("ul"); + var theme = completion.cm.options.theme; + hints.className = "CodeMirror-hints " + theme; + this.selectedHint = data.selectedHint || 0; + + var completions = data.list; + for (var i = 0; i < completions.length; ++i) { + var elt = hints.appendChild(ownerDocument.createElement("li")), cur = completions[i]; + var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); + if (cur.className != null) className = cur.className + " " + className; + elt.className = className; + if (cur.render) cur.render(elt, data, cur); + else elt.appendChild(ownerDocument.createTextNode(cur.displayText || getText(cur))); + elt.hintId = i; + } + + var container = completion.options.container || ownerDocument.body; + var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); + var left = pos.left, top = pos.bottom, below = true; + var offsetLeft = 0, offsetTop = 0; + if (container !== ownerDocument.body) { + // We offset the cursor position because left and top are relative to the offsetParent's top left corner. + var isContainerPositioned = ['absolute', 'relative', 'fixed'].indexOf(parentWindow.getComputedStyle(container).position) !== -1; + var offsetParent = isContainerPositioned ? container : container.offsetParent; + var offsetParentPosition = offsetParent.getBoundingClientRect(); + var bodyPosition = ownerDocument.body.getBoundingClientRect(); + offsetLeft = (offsetParentPosition.left - bodyPosition.left - offsetParent.scrollLeft); + offsetTop = (offsetParentPosition.top - bodyPosition.top - offsetParent.scrollTop); + } + hints.style.left = (left - offsetLeft) + "px"; + hints.style.top = (top - offsetTop) + "px"; + + // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. + var winW = parentWindow.innerWidth || Math.max(ownerDocument.body.offsetWidth, ownerDocument.documentElement.offsetWidth); + var winH = parentWindow.innerHeight || Math.max(ownerDocument.body.offsetHeight, ownerDocument.documentElement.offsetHeight); + container.appendChild(hints); + var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH; + var scrolls = hints.scrollHeight > hints.clientHeight + 1 + var startScroll = cm.getScrollInfo(); + + if (overlapY > 0) { + var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top); + if (curTop - height > 0) { // Fits above cursor + hints.style.top = (top = pos.top - height - offsetTop) + "px"; + below = false; + } else if (height > winH) { + hints.style.height = (winH - 5) + "px"; + hints.style.top = (top = pos.bottom - box.top - offsetTop) + "px"; + var cursor = cm.getCursor(); + if (data.from.ch != cursor.ch) { + pos = cm.cursorCoords(cursor); + hints.style.left = (left = pos.left - offsetLeft) + "px"; + box = hints.getBoundingClientRect(); + } + } + } + var overlapX = box.right - winW; + if (overlapX > 0) { + if (box.right - box.left > winW) { + hints.style.width = (winW - 5) + "px"; + overlapX -= (box.right - box.left) - winW; + } + hints.style.left = (left = pos.left - overlapX - offsetLeft) + "px"; + } + if (scrolls) for (var node = hints.firstChild; node; node = node.nextSibling) + node.style.paddingRight = cm.display.nativeBarWidth + "px" + + cm.addKeyMap(this.keyMap = buildKeyMap(completion, { + moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, + setFocus: function(n) { widget.changeActive(n); }, + menuSize: function() { return widget.screenAmount(); }, + length: completions.length, + close: function() { completion.close(); }, + pick: function() { widget.pick(); }, + data: data + })); + + if (completion.options.closeOnUnfocus) { + var closingOnBlur; + cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); + cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); + } + + cm.on("scroll", this.onScroll = function() { + var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); + var newTop = top + startScroll.top - curScroll.top; + var point = newTop - (parentWindow.pageYOffset || (ownerDocument.documentElement || ownerDocument.body).scrollTop); + if (!below) point += hints.offsetHeight; + if (point <= editor.top || point >= editor.bottom) return completion.close(); + hints.style.top = newTop + "px"; + hints.style.left = (left + startScroll.left - curScroll.left) + "px"; + }); + + CodeMirror.on(hints, "dblclick", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} + }); + + CodeMirror.on(hints, "click", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) { + widget.changeActive(t.hintId); + if (completion.options.completeOnSingleClick) widget.pick(); + } + }); + + CodeMirror.on(hints, "mousedown", function() { + setTimeout(function(){cm.focus();}, 20); + }); + + CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]); + return true; + } + + Widget.prototype = { + close: function() { + if (this.completion.widget != this) return; + this.completion.widget = null; + this.hints.parentNode.removeChild(this.hints); + this.completion.cm.removeKeyMap(this.keyMap); + + var cm = this.completion.cm; + if (this.completion.options.closeOnUnfocus) { + cm.off("blur", this.onBlur); + cm.off("focus", this.onFocus); + } + cm.off("scroll", this.onScroll); + }, + + disable: function() { + this.completion.cm.removeKeyMap(this.keyMap); + var widget = this; + this.keyMap = {Enter: function() { widget.picked = true; }}; + this.completion.cm.addKeyMap(this.keyMap); + }, + + pick: function() { + this.completion.pick(this.data, this.selectedHint); + }, + + changeActive: function(i, avoidWrap) { + if (i >= this.data.list.length) + i = avoidWrap ? this.data.list.length - 1 : 0; + else if (i < 0) + i = avoidWrap ? 0 : this.data.list.length - 1; + if (this.selectedHint == i) return; + var node = this.hints.childNodes[this.selectedHint]; + if (node) node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); + node = this.hints.childNodes[this.selectedHint = i]; + node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; + if (node.offsetTop < this.hints.scrollTop) + this.hints.scrollTop = node.offsetTop - 3; + else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) + this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; + CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); + }, + + screenAmount: function() { + return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; + } + }; + + function applicableHelpers(cm, helpers) { + if (!cm.somethingSelected()) return helpers + var result = [] + for (var i = 0; i < helpers.length; i++) + if (helpers[i].supportsSelection) result.push(helpers[i]) + return result + } + + function fetchHints(hint, cm, options, callback) { + if (hint.async) { + hint(cm, callback, options) + } else { + var result = hint(cm, options) + if (result && result.then) result.then(callback) + else callback(result) + } + } + + function resolveAutoHints(cm, pos) { + var helpers = cm.getHelpers(pos, "hint"), words + if (helpers.length) { + var resolved = function(cm, callback, options) { + var app = applicableHelpers(cm, helpers); + function run(i) { + if (i == app.length) return callback(null) + fetchHints(app[i], cm, options, function(result) { + if (result && result.list.length > 0) callback(result) + else run(i + 1) + }) + } + run(0) + } + resolved.async = true + resolved.supportsSelection = true + return resolved + } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { + return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) } + } else if (CodeMirror.hint.anyword) { + return function(cm, options) { return CodeMirror.hint.anyword(cm, options) } + } else { + return function() {} + } + } + + CodeMirror.registerHelper("hint", "auto", { + resolve: resolveAutoHints + }); + + CodeMirror.registerHelper("hint", "fromList", function(cm, options) { + var cur = cm.getCursor(), token = cm.getTokenAt(cur) + var term, from = CodeMirror.Pos(cur.line, token.start), to = cur + if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) { + term = token.string.substr(0, cur.ch - token.start) + } else { + term = "" + from = cur + } + var found = []; + for (var i = 0; i < options.words.length; i++) { + var word = options.words[i]; + if (word.slice(0, term.length) == term) + found.push(word); + } + + if (found.length) return {list: found, from: from, to: to}; + }); + + CodeMirror.commands.autocomplete = CodeMirror.showHint; + + var defaultOptions = { + hint: CodeMirror.hint.auto, + completeSingle: true, + alignWithWord: true, + closeCharacters: /[\s()\[\]{};:>,]/, + closeOnUnfocus: true, + completeOnSingleClick: true, + container: null, + customKeys: null, + extraKeys: null + }; + + CodeMirror.defineOption("hintOptions", null); +}); diff --git a/public/vendor/plugins/codemirror/addon/hint/sql-hint.js b/public/vendor/plugins/codemirror/addon/hint/sql-hint.js new file mode 100644 index 0000000000..444eba8b15 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/hint/sql-hint.js @@ -0,0 +1,304 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../../mode/sql/sql")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../../mode/sql/sql"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var tables; + var defaultTable; + var keywords; + var identifierQuote; + var CONS = { + QUERY_DIV: ";", + ALIAS_KEYWORD: "AS" + }; + var Pos = CodeMirror.Pos, cmpPos = CodeMirror.cmpPos; + + function isArray(val) { return Object.prototype.toString.call(val) == "[object Array]" } + + function getKeywords(editor) { + var mode = editor.doc.modeOption; + if (mode === "sql") mode = "text/x-sql"; + return CodeMirror.resolveMode(mode).keywords; + } + + function getIdentifierQuote(editor) { + var mode = editor.doc.modeOption; + if (mode === "sql") mode = "text/x-sql"; + return CodeMirror.resolveMode(mode).identifierQuote || "`"; + } + + function getText(item) { + return typeof item == "string" ? item : item.text; + } + + function wrapTable(name, value) { + if (isArray(value)) value = {columns: value} + if (!value.text) value.text = name + return value + } + + function parseTables(input) { + var result = {} + if (isArray(input)) { + for (var i = input.length - 1; i >= 0; i--) { + var item = input[i] + result[getText(item).toUpperCase()] = wrapTable(getText(item), item) + } + } else if (input) { + for (var name in input) + result[name.toUpperCase()] = wrapTable(name, input[name]) + } + return result + } + + function getTable(name) { + return tables[name.toUpperCase()] + } + + function shallowClone(object) { + var result = {}; + for (var key in object) if (object.hasOwnProperty(key)) + result[key] = object[key]; + return result; + } + + function match(string, word) { + var len = string.length; + var sub = getText(word).substr(0, len); + return string.toUpperCase() === sub.toUpperCase(); + } + + function addMatches(result, search, wordlist, formatter) { + if (isArray(wordlist)) { + for (var i = 0; i < wordlist.length; i++) + if (match(search, wordlist[i])) result.push(formatter(wordlist[i])) + } else { + for (var word in wordlist) if (wordlist.hasOwnProperty(word)) { + var val = wordlist[word] + if (!val || val === true) + val = word + else + val = val.displayText ? {text: val.text, displayText: val.displayText} : val.text + if (match(search, val)) result.push(formatter(val)) + } + } + } + + function cleanName(name) { + // Get rid name from identifierQuote and preceding dot(.) + if (name.charAt(0) == ".") { + name = name.substr(1); + } + // replace doublicated identifierQuotes with single identifierQuotes + // and remove single identifierQuotes + var nameParts = name.split(identifierQuote+identifierQuote); + for (var i = 0; i < nameParts.length; i++) + nameParts[i] = nameParts[i].replace(new RegExp(identifierQuote,"g"), ""); + return nameParts.join(identifierQuote); + } + + function insertIdentifierQuotes(name) { + var nameParts = getText(name).split("."); + for (var i = 0; i < nameParts.length; i++) + nameParts[i] = identifierQuote + + // doublicate identifierQuotes + nameParts[i].replace(new RegExp(identifierQuote,"g"), identifierQuote+identifierQuote) + + identifierQuote; + var escaped = nameParts.join("."); + if (typeof name == "string") return escaped; + name = shallowClone(name); + name.text = escaped; + return name; + } + + function nameCompletion(cur, token, result, editor) { + // Try to complete table, column names and return start position of completion + var useIdentifierQuotes = false; + var nameParts = []; + var start = token.start; + var cont = true; + while (cont) { + cont = (token.string.charAt(0) == "."); + useIdentifierQuotes = useIdentifierQuotes || (token.string.charAt(0) == identifierQuote); + + start = token.start; + nameParts.unshift(cleanName(token.string)); + + token = editor.getTokenAt(Pos(cur.line, token.start)); + if (token.string == ".") { + cont = true; + token = editor.getTokenAt(Pos(cur.line, token.start)); + } + } + + // Try to complete table names + var string = nameParts.join("."); + addMatches(result, string, tables, function(w) { + return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; + }); + + // Try to complete columns from defaultTable + addMatches(result, string, defaultTable, function(w) { + return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; + }); + + // Try to complete columns + string = nameParts.pop(); + var table = nameParts.join("."); + + var alias = false; + var aliasTable = table; + // Check if table is available. If not, find table by Alias + if (!getTable(table)) { + var oldTable = table; + table = findTableByAlias(table, editor); + if (table !== oldTable) alias = true; + } + + var columns = getTable(table); + if (columns && columns.columns) + columns = columns.columns; + + if (columns) { + addMatches(result, string, columns, function(w) { + var tableInsert = table; + if (alias == true) tableInsert = aliasTable; + if (typeof w == "string") { + w = tableInsert + "." + w; + } else { + w = shallowClone(w); + w.text = tableInsert + "." + w.text; + } + return useIdentifierQuotes ? insertIdentifierQuotes(w) : w; + }); + } + + return start; + } + + function eachWord(lineText, f) { + var words = lineText.split(/\s+/) + for (var i = 0; i < words.length; i++) + if (words[i]) f(words[i].replace(/[,;]/g, '')) + } + + function findTableByAlias(alias, editor) { + var doc = editor.doc; + var fullQuery = doc.getValue(); + var aliasUpperCase = alias.toUpperCase(); + var previousWord = ""; + var table = ""; + var separator = []; + var validRange = { + start: Pos(0, 0), + end: Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).length) + }; + + //add separator + var indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV); + while(indexOfSeparator != -1) { + separator.push(doc.posFromIndex(indexOfSeparator)); + indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV, indexOfSeparator+1); + } + separator.unshift(Pos(0, 0)); + separator.push(Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).text.length)); + + //find valid range + var prevItem = null; + var current = editor.getCursor() + for (var i = 0; i < separator.length; i++) { + if ((prevItem == null || cmpPos(current, prevItem) > 0) && cmpPos(current, separator[i]) <= 0) { + validRange = {start: prevItem, end: separator[i]}; + break; + } + prevItem = separator[i]; + } + + if (validRange.start) { + var query = doc.getRange(validRange.start, validRange.end, false); + + for (var i = 0; i < query.length; i++) { + var lineText = query[i]; + eachWord(lineText, function(word) { + var wordUpperCase = word.toUpperCase(); + if (wordUpperCase === aliasUpperCase && getTable(previousWord)) + table = previousWord; + if (wordUpperCase !== CONS.ALIAS_KEYWORD) + previousWord = word; + }); + if (table) break; + } + } + return table; + } + + CodeMirror.registerHelper("hint", "sql", function(editor, options) { + tables = parseTables(options && options.tables) + var defaultTableName = options && options.defaultTable; + var disableKeywords = options && options.disableKeywords; + defaultTable = defaultTableName && getTable(defaultTableName); + keywords = getKeywords(editor); + identifierQuote = getIdentifierQuote(editor); + + if (defaultTableName && !defaultTable) + defaultTable = findTableByAlias(defaultTableName, editor); + + defaultTable = defaultTable || []; + + if (defaultTable.columns) + defaultTable = defaultTable.columns; + + var cur = editor.getCursor(); + var result = []; + var token = editor.getTokenAt(cur), start, end, search; + if (token.end > cur.ch) { + token.end = cur.ch; + token.string = token.string.slice(0, cur.ch - token.start); + } + + if (token.string.match(/^[.`"\w@]\w*$/)) { + search = token.string; + start = token.start; + end = token.end; + } else { + start = end = cur.ch; + search = ""; + } + if (search.charAt(0) == "." || search.charAt(0) == identifierQuote) { + start = nameCompletion(cur, token, result, editor); + } else { + var objectOrClass = function(w, className) { + if (typeof w === "object") { + w.className = className; + } else { + w = { text: w, className: className }; + } + return w; + }; + addMatches(result, search, defaultTable, function(w) { + return objectOrClass(w, "CodeMirror-hint-table CodeMirror-hint-default-table"); + }); + addMatches( + result, + search, + tables, function(w) { + return objectOrClass(w, "CodeMirror-hint-table"); + } + ); + if (!disableKeywords) + addMatches(result, search, keywords, function(w) { + return objectOrClass(w.toUpperCase(), "CodeMirror-hint-keyword"); + }); + } + + return {list: result, from: Pos(cur.line, start), to: Pos(cur.line, end)}; + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/hint/xml-hint.js b/public/vendor/plugins/codemirror/addon/hint/xml-hint.js new file mode 100644 index 0000000000..7575b3707e --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/hint/xml-hint.js @@ -0,0 +1,123 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var Pos = CodeMirror.Pos; + + function matches(hint, typed, matchInMiddle) { + if (matchInMiddle) return hint.indexOf(typed) >= 0; + else return hint.lastIndexOf(typed, 0) == 0; + } + + function getHints(cm, options) { + var tags = options && options.schemaInfo; + var quote = (options && options.quoteChar) || '"'; + var matchInMiddle = options && options.matchInMiddle; + if (!tags) return; + var cur = cm.getCursor(), token = cm.getTokenAt(cur); + if (token.end > cur.ch) { + token.end = cur.ch; + token.string = token.string.slice(0, cur.ch - token.start); + } + var inner = CodeMirror.innerMode(cm.getMode(), token.state); + if (!inner.mode.xmlCurrentTag) return + var result = [], replaceToken = false, prefix; + var tag = /\btag\b/.test(token.type) && !/>$/.test(token.string); + var tagName = tag && /^\w/.test(token.string), tagStart; + + if (tagName) { + var before = cm.getLine(cur.line).slice(Math.max(0, token.start - 2), token.start); + var tagType = /<\/$/.test(before) ? "close" : /<$/.test(before) ? "open" : null; + if (tagType) tagStart = token.start - (tagType == "close" ? 2 : 1); + } else if (tag && token.string == "<") { + tagType = "open"; + } else if (tag && token.string == ""); + } else { + // Attribute completion + var curTag = tagInfo && tags[tagInfo.name], attrs = curTag && curTag.attrs; + var globalAttrs = tags["!attrs"]; + if (!attrs && !globalAttrs) return; + if (!attrs) { + attrs = globalAttrs; + } else if (globalAttrs) { // Combine tag-local and global attributes + var set = {}; + for (var nm in globalAttrs) if (globalAttrs.hasOwnProperty(nm)) set[nm] = globalAttrs[nm]; + for (var nm in attrs) if (attrs.hasOwnProperty(nm)) set[nm] = attrs[nm]; + attrs = set; + } + if (token.type == "string" || token.string == "=") { // A value + var before = cm.getRange(Pos(cur.line, Math.max(0, cur.ch - 60)), + Pos(cur.line, token.type == "string" ? token.start : token.end)); + var atName = before.match(/([^\s\u00a0=<>\"\']+)=$/), atValues; + if (!atName || !attrs.hasOwnProperty(atName[1]) || !(atValues = attrs[atName[1]])) return; + if (typeof atValues == 'function') atValues = atValues.call(this, cm); // Functions can be used to supply values for autocomplete widget + if (token.type == "string") { + prefix = token.string; + var n = 0; + if (/['"]/.test(token.string.charAt(0))) { + quote = token.string.charAt(0); + prefix = token.string.slice(1); + n++; + } + var len = token.string.length; + if (/['"]/.test(token.string.charAt(len - 1))) { + quote = token.string.charAt(len - 1); + prefix = token.string.substr(n, len - 2); + } + if (n) { // an opening quote + var line = cm.getLine(cur.line); + if (line.length > token.end && line.charAt(token.end) == quote) token.end++; // include a closing quote + } + replaceToken = true; + } + for (var i = 0; i < atValues.length; ++i) if (!prefix || matches(atValues[i], prefix, matchInMiddle)) + result.push(quote + atValues[i] + quote); + } else { // An attribute name + if (token.type == "attribute") { + prefix = token.string; + replaceToken = true; + } + for (var attr in attrs) if (attrs.hasOwnProperty(attr) && (!prefix || matches(attr, prefix, matchInMiddle))) + result.push(attr); + } + } + return { + list: result, + from: replaceToken ? Pos(cur.line, tagStart == null ? token.start : tagStart) : cur, + to: replaceToken ? Pos(cur.line, token.end) : cur + }; + } + + CodeMirror.registerHelper("hint", "xml", getHints); +}); diff --git a/public/vendor/plugins/codemirror/addon/lint/coffeescript-lint.js b/public/vendor/plugins/codemirror/addon/lint/coffeescript-lint.js new file mode 100644 index 0000000000..a54c703516 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/lint/coffeescript-lint.js @@ -0,0 +1,47 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Depends on coffeelint.js from http://www.coffeelint.org/js/coffeelint.js + +// declare global: coffeelint + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerHelper("lint", "coffeescript", function(text) { + var found = []; + if (!window.coffeelint) { + if (window.console) { + window.console.error("Error: window.coffeelint not defined, CodeMirror CoffeeScript linting cannot run."); + } + return found; + } + var parseError = function(err) { + var loc = err.lineNumber; + found.push({from: CodeMirror.Pos(loc-1, 0), + to: CodeMirror.Pos(loc, 0), + severity: err.level, + message: err.message}); + }; + try { + var res = coffeelint.lint(text); + for(var i = 0; i < res.length; i++) { + parseError(res[i]); + } + } catch(e) { + found.push({from: CodeMirror.Pos(e.location.first_line, 0), + to: CodeMirror.Pos(e.location.last_line, e.location.last_column), + severity: 'error', + message: e.message}); + } + return found; +}); + +}); diff --git a/public/vendor/plugins/codemirror/addon/lint/css-lint.js b/public/vendor/plugins/codemirror/addon/lint/css-lint.js new file mode 100644 index 0000000000..6058a73eb1 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/lint/css-lint.js @@ -0,0 +1,40 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Depends on csslint.js from https://github.com/stubbornella/csslint + +// declare global: CSSLint + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerHelper("lint", "css", function(text, options) { + var found = []; + if (!window.CSSLint) { + if (window.console) { + window.console.error("Error: window.CSSLint not defined, CodeMirror CSS linting cannot run."); + } + return found; + } + var results = CSSLint.verify(text, options), messages = results.messages, message = null; + for ( var i = 0; i < messages.length; i++) { + message = messages[i]; + var startLine = message.line -1, endLine = message.line -1, startCol = message.col -1, endCol = message.col; + found.push({ + from: CodeMirror.Pos(startLine, startCol), + to: CodeMirror.Pos(endLine, endCol), + message: message.message, + severity : message.type + }); + } + return found; +}); + +}); diff --git a/public/vendor/plugins/codemirror/addon/lint/html-lint.js b/public/vendor/plugins/codemirror/addon/lint/html-lint.js new file mode 100644 index 0000000000..5295c33331 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/lint/html-lint.js @@ -0,0 +1,59 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Depends on htmlhint.js from http://htmlhint.com/js/htmlhint.js + +// declare global: HTMLHint + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("htmlhint")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "htmlhint"], mod); + else // Plain browser env + mod(CodeMirror, window.HTMLHint); +})(function(CodeMirror, HTMLHint) { + "use strict"; + + var defaultRules = { + "tagname-lowercase": true, + "attr-lowercase": true, + "attr-value-double-quotes": true, + "doctype-first": false, + "tag-pair": true, + "spec-char-escape": true, + "id-unique": true, + "src-not-empty": true, + "attr-no-duplication": true + }; + + CodeMirror.registerHelper("lint", "html", function(text, options) { + var found = []; + if (HTMLHint && !HTMLHint.verify) { + if(typeof HTMLHint.default !== 'undefined') { + HTMLHint = HTMLHint.default; + } else { + HTMLHint = HTMLHint.HTMLHint; + } + } + if (!HTMLHint) HTMLHint = window.HTMLHint; + if (!HTMLHint) { + if (window.console) { + window.console.error("Error: HTMLHint not found, not defined on window, or not available through define/require, CodeMirror HTML linting cannot run."); + } + return found; + } + var messages = HTMLHint.verify(text, options && options.rules || defaultRules); + for (var i = 0; i < messages.length; i++) { + var message = messages[i]; + var startLine = message.line - 1, endLine = message.line - 1, startCol = message.col - 1, endCol = message.col; + found.push({ + from: CodeMirror.Pos(startLine, startCol), + to: CodeMirror.Pos(endLine, endCol), + message: message.message, + severity : message.type + }); + } + return found; + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/lint/javascript-lint.js b/public/vendor/plugins/codemirror/addon/lint/javascript-lint.js new file mode 100644 index 0000000000..cc132d7f82 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/lint/javascript-lint.js @@ -0,0 +1,63 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + // declare global: JSHINT + + function validator(text, options) { + if (!window.JSHINT) { + if (window.console) { + window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run."); + } + return []; + } + if (!options.indent) // JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation + options.indent = 1; // JSHint default value is 4 + JSHINT(text, options, options.globals); + var errors = JSHINT.data().errors, result = []; + if (errors) parseErrors(errors, result); + return result; + } + + CodeMirror.registerHelper("lint", "javascript", validator); + + function parseErrors(errors, output) { + for ( var i = 0; i < errors.length; i++) { + var error = errors[i]; + if (error) { + if (error.line <= 0) { + if (window.console) { + window.console.warn("Cannot display JSHint error (invalid line " + error.line + ")", error); + } + continue; + } + + var start = error.character - 1, end = start + 1; + if (error.evidence) { + var index = error.evidence.substring(start).search(/.\b/); + if (index > -1) { + end += index; + } + } + + // Convert to format expected by validation service + var hint = { + message: error.reason, + severity: error.code ? (error.code.startsWith('W') ? "warning" : "error") : "error", + from: CodeMirror.Pos(error.line - 1, start), + to: CodeMirror.Pos(error.line - 1, end) + }; + + output.push(hint); + } + } + } +}); diff --git a/public/vendor/plugins/codemirror/addon/lint/json-lint.js b/public/vendor/plugins/codemirror/addon/lint/json-lint.js new file mode 100644 index 0000000000..ac1d6ec28c --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/lint/json-lint.js @@ -0,0 +1,40 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Depends on jsonlint.js from https://github.com/zaach/jsonlint + +// declare global: jsonlint + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.registerHelper("lint", "json", function(text) { + var found = []; + if (!window.jsonlint) { + if (window.console) { + window.console.error("Error: window.jsonlint not defined, CodeMirror JSON linting cannot run."); + } + return found; + } + // for jsonlint's web dist jsonlint is exported as an object with a single property parser, of which parseError + // is a subproperty + var jsonlint = window.jsonlint.parser || window.jsonlint + jsonlint.parseError = function(str, hash) { + var loc = hash.loc; + found.push({from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), + to: CodeMirror.Pos(loc.last_line - 1, loc.last_column), + message: str}); + }; + try { jsonlint.parse(text); } + catch(e) {} + return found; +}); + +}); diff --git a/public/vendor/plugins/codemirror/addon/lint/lint.css b/public/vendor/plugins/codemirror/addon/lint/lint.css new file mode 100644 index 0000000000..f097cfe345 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/lint/lint.css @@ -0,0 +1,73 @@ +/* The lint marker gutter */ +.CodeMirror-lint-markers { + width: 16px; +} + +.CodeMirror-lint-tooltip { + background-color: #ffd; + border: 1px solid black; + border-radius: 4px 4px 4px 4px; + color: black; + font-family: monospace; + font-size: 10pt; + overflow: hidden; + padding: 2px 5px; + position: fixed; + white-space: pre; + white-space: pre-wrap; + z-index: 100; + max-width: 600px; + opacity: 0; + transition: opacity .4s; + -moz-transition: opacity .4s; + -webkit-transition: opacity .4s; + -o-transition: opacity .4s; + -ms-transition: opacity .4s; +} + +.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { + background-position: left bottom; + background-repeat: repeat-x; +} + +.CodeMirror-lint-mark-error { + background-image: + url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") + ; +} + +.CodeMirror-lint-mark-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; + display: inline-block; + height: 16px; + width: 16px; + vertical-align: middle; + position: relative; +} + +.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { + padding-left: 18px; + background-position: top left; + background-repeat: no-repeat; +} + +.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-multiple { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); + background-repeat: no-repeat; + background-position: right bottom; + width: 100%; height: 100%; +} diff --git a/public/vendor/plugins/codemirror/addon/lint/lint.js b/public/vendor/plugins/codemirror/addon/lint/lint.js new file mode 100644 index 0000000000..aa75ba0e8a --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/lint/lint.js @@ -0,0 +1,252 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + var GUTTER_ID = "CodeMirror-lint-markers"; + + function showTooltip(e, content) { + var tt = document.createElement("div"); + tt.className = "CodeMirror-lint-tooltip"; + tt.appendChild(content.cloneNode(true)); + document.body.appendChild(tt); + + function position(e) { + if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position); + tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px"; + tt.style.left = (e.clientX + 5) + "px"; + } + CodeMirror.on(document, "mousemove", position); + position(e); + if (tt.style.opacity != null) tt.style.opacity = 1; + return tt; + } + function rm(elt) { + if (elt.parentNode) elt.parentNode.removeChild(elt); + } + function hideTooltip(tt) { + if (!tt.parentNode) return; + if (tt.style.opacity == null) rm(tt); + tt.style.opacity = 0; + setTimeout(function() { rm(tt); }, 600); + } + + function showTooltipFor(e, content, node) { + var tooltip = showTooltip(e, content); + function hide() { + CodeMirror.off(node, "mouseout", hide); + if (tooltip) { hideTooltip(tooltip); tooltip = null; } + } + var poll = setInterval(function() { + if (tooltip) for (var n = node;; n = n.parentNode) { + if (n && n.nodeType == 11) n = n.host; + if (n == document.body) return; + if (!n) { hide(); break; } + } + if (!tooltip) return clearInterval(poll); + }, 400); + CodeMirror.on(node, "mouseout", hide); + } + + function LintState(cm, options, hasGutter) { + this.marked = []; + this.options = options; + this.timeout = null; + this.hasGutter = hasGutter; + this.onMouseOver = function(e) { onMouseOver(cm, e); }; + this.waitingFor = 0 + } + + function parseOptions(_cm, options) { + if (options instanceof Function) return {getAnnotations: options}; + if (!options || options === true) options = {}; + return options; + } + + function clearMarks(cm) { + var state = cm.state.lint; + if (state.hasGutter) cm.clearGutter(GUTTER_ID); + for (var i = 0; i < state.marked.length; ++i) + state.marked[i].clear(); + state.marked.length = 0; + } + + function makeMarker(labels, severity, multiple, tooltips) { + var marker = document.createElement("div"), inner = marker; + marker.className = "CodeMirror-lint-marker-" + severity; + if (multiple) { + inner = marker.appendChild(document.createElement("div")); + inner.className = "CodeMirror-lint-marker-multiple"; + } + + if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) { + showTooltipFor(e, labels, inner); + }); + + return marker; + } + + function getMaxSeverity(a, b) { + if (a == "error") return a; + else return b; + } + + function groupByLine(annotations) { + var lines = []; + for (var i = 0; i < annotations.length; ++i) { + var ann = annotations[i], line = ann.from.line; + (lines[line] || (lines[line] = [])).push(ann); + } + return lines; + } + + function annotationTooltip(ann) { + var severity = ann.severity; + if (!severity) severity = "error"; + var tip = document.createElement("div"); + tip.className = "CodeMirror-lint-message-" + severity; + if (typeof ann.messageHTML != 'undefined') { + tip.innerHTML = ann.messageHTML; + } else { + tip.appendChild(document.createTextNode(ann.message)); + } + return tip; + } + + function lintAsync(cm, getAnnotations, passOptions) { + var state = cm.state.lint + var id = ++state.waitingFor + function abort() { + id = -1 + cm.off("change", abort) + } + cm.on("change", abort) + getAnnotations(cm.getValue(), function(annotations, arg2) { + cm.off("change", abort) + if (state.waitingFor != id) return + if (arg2 && annotations instanceof CodeMirror) annotations = arg2 + cm.operation(function() {updateLinting(cm, annotations)}) + }, passOptions, cm); + } + + function startLinting(cm) { + var state = cm.state.lint, options = state.options; + /* + * Passing rules in `options` property prevents JSHint (and other linters) from complaining + * about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc. + */ + var passOptions = options.options || options; + var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint"); + if (!getAnnotations) return; + if (options.async || getAnnotations.async) { + lintAsync(cm, getAnnotations, passOptions) + } else { + var annotations = getAnnotations(cm.getValue(), passOptions, cm); + if (!annotations) return; + if (annotations.then) annotations.then(function(issues) { + cm.operation(function() {updateLinting(cm, issues)}) + }); + else cm.operation(function() {updateLinting(cm, annotations)}) + } + } + + function updateLinting(cm, annotationsNotSorted) { + clearMarks(cm); + var state = cm.state.lint, options = state.options; + + var annotations = groupByLine(annotationsNotSorted); + + for (var line = 0; line < annotations.length; ++line) { + var anns = annotations[line]; + if (!anns) continue; + + var maxSeverity = null; + var tipLabel = state.hasGutter && document.createDocumentFragment(); + + for (var i = 0; i < anns.length; ++i) { + var ann = anns[i]; + var severity = ann.severity; + if (!severity) severity = "error"; + maxSeverity = getMaxSeverity(maxSeverity, severity); + + if (options.formatAnnotation) ann = options.formatAnnotation(ann); + if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann)); + + if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, { + className: "CodeMirror-lint-mark-" + severity, + __annotation: ann + })); + } + + if (state.hasGutter) + cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1, + state.options.tooltips)); + } + if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm); + } + + function onChange(cm) { + var state = cm.state.lint; + if (!state) return; + clearTimeout(state.timeout); + state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500); + } + + function popupTooltips(annotations, e) { + var target = e.target || e.srcElement; + var tooltip = document.createDocumentFragment(); + for (var i = 0; i < annotations.length; i++) { + var ann = annotations[i]; + tooltip.appendChild(annotationTooltip(ann)); + } + showTooltipFor(e, tooltip, target); + } + + function onMouseOver(cm, e) { + var target = e.target || e.srcElement; + if (!/\bCodeMirror-lint-mark-/.test(target.className)) return; + var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2; + var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client")); + + var annotations = []; + for (var i = 0; i < spans.length; ++i) { + var ann = spans[i].__annotation; + if (ann) annotations.push(ann); + } + if (annotations.length) popupTooltips(annotations, e); + } + + CodeMirror.defineOption("lint", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + clearMarks(cm); + if (cm.state.lint.options.lintOnChange !== false) + cm.off("change", onChange); + CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver); + clearTimeout(cm.state.lint.timeout); + delete cm.state.lint; + } + + if (val) { + var gutters = cm.getOption("gutters"), hasLintGutter = false; + for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true; + var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter); + if (state.options.lintOnChange !== false) + cm.on("change", onChange); + if (state.options.tooltips != false && state.options.tooltips != "gutter") + CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver); + + startLinting(cm); + } + }); + + CodeMirror.defineExtension("performLint", function() { + if (this.state.lint) startLinting(this); + }); +}); diff --git a/public/vendor/plugins/codemirror/addon/lint/yaml-lint.js b/public/vendor/plugins/codemirror/addon/lint/yaml-lint.js new file mode 100644 index 0000000000..b4ac5abc4e --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/lint/yaml-lint.js @@ -0,0 +1,41 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +// Depends on js-yaml.js from https://github.com/nodeca/js-yaml + +// declare global: jsyaml + +CodeMirror.registerHelper("lint", "yaml", function(text) { + var found = []; + if (!window.jsyaml) { + if (window.console) { + window.console.error("Error: window.jsyaml not defined, CodeMirror YAML linting cannot run."); + } + return found; + } + try { jsyaml.loadAll(text); } + catch(e) { + var loc = e.mark, + // js-yaml YAMLException doesn't always provide an accurate lineno + // e.g., when there are multiple yaml docs + // --- + // --- + // foo:bar + from = loc ? CodeMirror.Pos(loc.line, loc.column) : CodeMirror.Pos(0, 0), + to = from; + found.push({ from: from, to: to, message: e.message }); + } + return found; +}); + +}); diff --git a/public/vendor/plugins/codemirror/addon/merge/merge.css b/public/vendor/plugins/codemirror/addon/merge/merge.css new file mode 100644 index 0000000000..dadd7f59c7 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/merge/merge.css @@ -0,0 +1,119 @@ +.CodeMirror-merge { + position: relative; + border: 1px solid #ddd; + white-space: pre; +} + +.CodeMirror-merge, .CodeMirror-merge .CodeMirror { + height: 350px; +} + +.CodeMirror-merge-2pane .CodeMirror-merge-pane { width: 47%; } +.CodeMirror-merge-2pane .CodeMirror-merge-gap { width: 6%; } +.CodeMirror-merge-3pane .CodeMirror-merge-pane { width: 31%; } +.CodeMirror-merge-3pane .CodeMirror-merge-gap { width: 3.5%; } + +.CodeMirror-merge-pane { + display: inline-block; + white-space: normal; + vertical-align: top; +} +.CodeMirror-merge-pane-rightmost { + position: absolute; + right: 0px; + z-index: 1; +} + +.CodeMirror-merge-gap { + z-index: 2; + display: inline-block; + height: 100%; + -moz-box-sizing: border-box; + box-sizing: border-box; + overflow: hidden; + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; + position: relative; + background: #f8f8f8; +} + +.CodeMirror-merge-scrolllock-wrap { + position: absolute; + bottom: 0; left: 50%; +} +.CodeMirror-merge-scrolllock { + position: relative; + left: -50%; + cursor: pointer; + color: #555; + line-height: 1; +} +.CodeMirror-merge-scrolllock:after { + content: "\21db\00a0\00a0\21da"; +} +.CodeMirror-merge-scrolllock.CodeMirror-merge-scrolllock-enabled:after { + content: "\21db\21da"; +} + +.CodeMirror-merge-copybuttons-left, .CodeMirror-merge-copybuttons-right { + position: absolute; + left: 0; top: 0; + right: 0; bottom: 0; + line-height: 1; +} + +.CodeMirror-merge-copy { + position: absolute; + cursor: pointer; + color: #44c; + z-index: 3; +} + +.CodeMirror-merge-copy-reverse { + position: absolute; + cursor: pointer; + color: #44c; +} + +.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy { left: 2px; } +.CodeMirror-merge-copybuttons-right .CodeMirror-merge-copy { right: 2px; } + +.CodeMirror-merge-r-inserted, .CodeMirror-merge-l-inserted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12MwuCXy3+CWyH8GBgYGJgYkAABZbAQ9ELXurwAAAABJRU5ErkJggg==); + background-position: bottom left; + background-repeat: repeat-x; +} + +.CodeMirror-merge-r-deleted, .CodeMirror-merge-l-deleted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12M4Kyb2/6yY2H8GBgYGJgYkAABURgPz6Ks7wQAAAABJRU5ErkJggg==); + background-position: bottom left; + background-repeat: repeat-x; +} + +.CodeMirror-merge-r-chunk { background: #ffffe0; } +.CodeMirror-merge-r-chunk-start { border-top: 1px solid #ee8; } +.CodeMirror-merge-r-chunk-end { border-bottom: 1px solid #ee8; } +.CodeMirror-merge-r-connect { fill: #ffffe0; stroke: #ee8; stroke-width: 1px; } + +.CodeMirror-merge-l-chunk { background: #eef; } +.CodeMirror-merge-l-chunk-start { border-top: 1px solid #88e; } +.CodeMirror-merge-l-chunk-end { border-bottom: 1px solid #88e; } +.CodeMirror-merge-l-connect { fill: #eef; stroke: #88e; stroke-width: 1px; } + +.CodeMirror-merge-l-chunk.CodeMirror-merge-r-chunk { background: #dfd; } +.CodeMirror-merge-l-chunk-start.CodeMirror-merge-r-chunk-start { border-top: 1px solid #4e4; } +.CodeMirror-merge-l-chunk-end.CodeMirror-merge-r-chunk-end { border-bottom: 1px solid #4e4; } + +.CodeMirror-merge-collapsed-widget:before { + content: "(...)"; +} +.CodeMirror-merge-collapsed-widget { + cursor: pointer; + color: #88b; + background: #eef; + border: 1px solid #ddf; + font-size: 90%; + padding: 0 3px; + border-radius: 4px; +} +.CodeMirror-merge-collapsed-line .CodeMirror-gutter-elt { display: none; } diff --git a/public/vendor/plugins/codemirror/addon/merge/merge.js b/public/vendor/plugins/codemirror/addon/merge/merge.js new file mode 100644 index 0000000000..8296540a05 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/merge/merge.js @@ -0,0 +1,1002 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// declare global: diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); // Note non-packaged dependency diff_match_patch + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "diff_match_patch"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + var Pos = CodeMirror.Pos; + var svgNS = "http://www.w3.org/2000/svg"; + + function DiffView(mv, type) { + this.mv = mv; + this.type = type; + this.classes = type == "left" + ? {chunk: "CodeMirror-merge-l-chunk", + start: "CodeMirror-merge-l-chunk-start", + end: "CodeMirror-merge-l-chunk-end", + insert: "CodeMirror-merge-l-inserted", + del: "CodeMirror-merge-l-deleted", + connect: "CodeMirror-merge-l-connect"} + : {chunk: "CodeMirror-merge-r-chunk", + start: "CodeMirror-merge-r-chunk-start", + end: "CodeMirror-merge-r-chunk-end", + insert: "CodeMirror-merge-r-inserted", + del: "CodeMirror-merge-r-deleted", + connect: "CodeMirror-merge-r-connect"}; + } + + DiffView.prototype = { + constructor: DiffView, + init: function(pane, orig, options) { + this.edit = this.mv.edit; + ;(this.edit.state.diffViews || (this.edit.state.diffViews = [])).push(this); + this.orig = CodeMirror(pane, copyObj({value: orig, readOnly: !this.mv.options.allowEditingOriginals}, copyObj(options))); + if (this.mv.options.connect == "align") { + if (!this.edit.state.trackAlignable) this.edit.state.trackAlignable = new TrackAlignable(this.edit) + this.orig.state.trackAlignable = new TrackAlignable(this.orig) + } + this.lockButton.title = this.edit.phrase("Toggle locked scrolling"); + + this.orig.state.diffViews = [this]; + var classLocation = options.chunkClassLocation || "background"; + if (Object.prototype.toString.call(classLocation) != "[object Array]") classLocation = [classLocation] + this.classes.classLocation = classLocation + + this.diff = getDiff(asString(orig), asString(options.value), this.mv.options.ignoreWhitespace); + this.chunks = getChunks(this.diff); + this.diffOutOfDate = this.dealigned = false; + this.needsScrollSync = null + + this.showDifferences = options.showDifferences !== false; + }, + registerEvents: function(otherDv) { + this.forceUpdate = registerUpdate(this); + setScrollLock(this, true, false); + registerScroll(this, otherDv); + }, + setShowDifferences: function(val) { + val = val !== false; + if (val != this.showDifferences) { + this.showDifferences = val; + this.forceUpdate("full"); + } + } + }; + + function ensureDiff(dv) { + if (dv.diffOutOfDate) { + dv.diff = getDiff(dv.orig.getValue(), dv.edit.getValue(), dv.mv.options.ignoreWhitespace); + dv.chunks = getChunks(dv.diff); + dv.diffOutOfDate = false; + CodeMirror.signal(dv.edit, "updateDiff", dv.diff); + } + } + + var updating = false; + function registerUpdate(dv) { + var edit = {from: 0, to: 0, marked: []}; + var orig = {from: 0, to: 0, marked: []}; + var debounceChange, updatingFast = false; + function update(mode) { + updating = true; + updatingFast = false; + if (mode == "full") { + if (dv.svg) clear(dv.svg); + if (dv.copyButtons) clear(dv.copyButtons); + clearMarks(dv.edit, edit.marked, dv.classes); + clearMarks(dv.orig, orig.marked, dv.classes); + edit.from = edit.to = orig.from = orig.to = 0; + } + ensureDiff(dv); + if (dv.showDifferences) { + updateMarks(dv.edit, dv.diff, edit, DIFF_INSERT, dv.classes); + updateMarks(dv.orig, dv.diff, orig, DIFF_DELETE, dv.classes); + } + + if (dv.mv.options.connect == "align") + alignChunks(dv); + makeConnections(dv); + if (dv.needsScrollSync != null) syncScroll(dv, dv.needsScrollSync) + + updating = false; + } + function setDealign(fast) { + if (updating) return; + dv.dealigned = true; + set(fast); + } + function set(fast) { + if (updating || updatingFast) return; + clearTimeout(debounceChange); + if (fast === true) updatingFast = true; + debounceChange = setTimeout(update, fast === true ? 20 : 250); + } + function change(_cm, change) { + if (!dv.diffOutOfDate) { + dv.diffOutOfDate = true; + edit.from = edit.to = orig.from = orig.to = 0; + } + // Update faster when a line was added/removed + setDealign(change.text.length - 1 != change.to.line - change.from.line); + } + function swapDoc() { + dv.diffOutOfDate = true; + dv.dealigned = true; + update("full"); + } + dv.edit.on("change", change); + dv.orig.on("change", change); + dv.edit.on("swapDoc", swapDoc); + dv.orig.on("swapDoc", swapDoc); + if (dv.mv.options.connect == "align") { + CodeMirror.on(dv.edit.state.trackAlignable, "realign", setDealign) + CodeMirror.on(dv.orig.state.trackAlignable, "realign", setDealign) + } + dv.edit.on("viewportChange", function() { set(false); }); + dv.orig.on("viewportChange", function() { set(false); }); + update(); + return update; + } + + function registerScroll(dv, otherDv) { + dv.edit.on("scroll", function() { + syncScroll(dv, true) && makeConnections(dv); + }); + dv.orig.on("scroll", function() { + syncScroll(dv, false) && makeConnections(dv); + if (otherDv) syncScroll(otherDv, true) && makeConnections(otherDv); + }); + } + + function syncScroll(dv, toOrig) { + // Change handler will do a refresh after a timeout when diff is out of date + if (dv.diffOutOfDate) { + if (dv.lockScroll && dv.needsScrollSync == null) dv.needsScrollSync = toOrig + return false + } + dv.needsScrollSync = null + if (!dv.lockScroll) return true; + var editor, other, now = +new Date; + if (toOrig) { editor = dv.edit; other = dv.orig; } + else { editor = dv.orig; other = dv.edit; } + // Don't take action if the position of this editor was recently set + // (to prevent feedback loops) + if (editor.state.scrollSetBy == dv && (editor.state.scrollSetAt || 0) + 250 > now) return false; + + var sInfo = editor.getScrollInfo(); + if (dv.mv.options.connect == "align") { + targetPos = sInfo.top; + } else { + var halfScreen = .5 * sInfo.clientHeight, midY = sInfo.top + halfScreen; + var mid = editor.lineAtHeight(midY, "local"); + var around = chunkBoundariesAround(dv.chunks, mid, toOrig); + var off = getOffsets(editor, toOrig ? around.edit : around.orig); + var offOther = getOffsets(other, toOrig ? around.orig : around.edit); + var ratio = (midY - off.top) / (off.bot - off.top); + var targetPos = (offOther.top - halfScreen) + ratio * (offOther.bot - offOther.top); + + var botDist, mix; + // Some careful tweaking to make sure no space is left out of view + // when scrolling to top or bottom. + if (targetPos > sInfo.top && (mix = sInfo.top / halfScreen) < 1) { + targetPos = targetPos * mix + sInfo.top * (1 - mix); + } else if ((botDist = sInfo.height - sInfo.clientHeight - sInfo.top) < halfScreen) { + var otherInfo = other.getScrollInfo(); + var botDistOther = otherInfo.height - otherInfo.clientHeight - targetPos; + if (botDistOther > botDist && (mix = botDist / halfScreen) < 1) + targetPos = targetPos * mix + (otherInfo.height - otherInfo.clientHeight - botDist) * (1 - mix); + } + } + + other.scrollTo(sInfo.left, targetPos); + other.state.scrollSetAt = now; + other.state.scrollSetBy = dv; + return true; + } + + function getOffsets(editor, around) { + var bot = around.after; + if (bot == null) bot = editor.lastLine() + 1; + return {top: editor.heightAtLine(around.before || 0, "local"), + bot: editor.heightAtLine(bot, "local")}; + } + + function setScrollLock(dv, val, action) { + dv.lockScroll = val; + if (val && action != false) syncScroll(dv, DIFF_INSERT) && makeConnections(dv); + (val ? CodeMirror.addClass : CodeMirror.rmClass)(dv.lockButton, "CodeMirror-merge-scrolllock-enabled"); + } + + // Updating the marks for editor content + + function removeClass(editor, line, classes) { + var locs = classes.classLocation + for (var i = 0; i < locs.length; i++) { + editor.removeLineClass(line, locs[i], classes.chunk); + editor.removeLineClass(line, locs[i], classes.start); + editor.removeLineClass(line, locs[i], classes.end); + } + } + + function clearMarks(editor, arr, classes) { + for (var i = 0; i < arr.length; ++i) { + var mark = arr[i]; + if (mark instanceof CodeMirror.TextMarker) + mark.clear(); + else if (mark.parent) + removeClass(editor, mark, classes); + } + arr.length = 0; + } + + // FIXME maybe add a margin around viewport to prevent too many updates + function updateMarks(editor, diff, state, type, classes) { + var vp = editor.getViewport(); + editor.operation(function() { + if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) { + clearMarks(editor, state.marked, classes); + markChanges(editor, diff, type, state.marked, vp.from, vp.to, classes); + state.from = vp.from; state.to = vp.to; + } else { + if (vp.from < state.from) { + markChanges(editor, diff, type, state.marked, vp.from, state.from, classes); + state.from = vp.from; + } + if (vp.to > state.to) { + markChanges(editor, diff, type, state.marked, state.to, vp.to, classes); + state.to = vp.to; + } + } + }); + } + + function addClass(editor, lineNr, classes, main, start, end) { + var locs = classes.classLocation, line = editor.getLineHandle(lineNr); + for (var i = 0; i < locs.length; i++) { + if (main) editor.addLineClass(line, locs[i], classes.chunk); + if (start) editor.addLineClass(line, locs[i], classes.start); + if (end) editor.addLineClass(line, locs[i], classes.end); + } + return line; + } + + function markChanges(editor, diff, type, marks, from, to, classes) { + var pos = Pos(0, 0); + var top = Pos(from, 0), bot = editor.clipPos(Pos(to - 1)); + var cls = type == DIFF_DELETE ? classes.del : classes.insert; + function markChunk(start, end) { + var bfrom = Math.max(from, start), bto = Math.min(to, end); + for (var i = bfrom; i < bto; ++i) + marks.push(addClass(editor, i, classes, true, i == start, i == end - 1)); + // When the chunk is empty, make sure a horizontal line shows up + if (start == end && bfrom == end && bto == end) { + if (bfrom) + marks.push(addClass(editor, bfrom - 1, classes, false, false, true)); + else + marks.push(addClass(editor, bfrom, classes, false, true, false)); + } + } + + var chunkStart = 0, pending = false; + for (var i = 0; i < diff.length; ++i) { + var part = diff[i], tp = part[0], str = part[1]; + if (tp == DIFF_EQUAL) { + var cleanFrom = pos.line + (startOfLineClean(diff, i) ? 0 : 1); + moveOver(pos, str); + var cleanTo = pos.line + (endOfLineClean(diff, i) ? 1 : 0); + if (cleanTo > cleanFrom) { + if (pending) { markChunk(chunkStart, cleanFrom); pending = false } + chunkStart = cleanTo; + } + } else { + pending = true + if (tp == type) { + var end = moveOver(pos, str, true); + var a = posMax(top, pos), b = posMin(bot, end); + if (!posEq(a, b)) + marks.push(editor.markText(a, b, {className: cls})); + pos = end; + } + } + } + if (pending) markChunk(chunkStart, pos.line + 1); + } + + // Updating the gap between editor and original + + function makeConnections(dv) { + if (!dv.showDifferences) return; + + if (dv.svg) { + clear(dv.svg); + var w = dv.gap.offsetWidth; + attrs(dv.svg, "width", w, "height", dv.gap.offsetHeight); + } + if (dv.copyButtons) clear(dv.copyButtons); + + var vpEdit = dv.edit.getViewport(), vpOrig = dv.orig.getViewport(); + var outerTop = dv.mv.wrap.getBoundingClientRect().top + var sTopEdit = outerTop - dv.edit.getScrollerElement().getBoundingClientRect().top + dv.edit.getScrollInfo().top + var sTopOrig = outerTop - dv.orig.getScrollerElement().getBoundingClientRect().top + dv.orig.getScrollInfo().top; + for (var i = 0; i < dv.chunks.length; i++) { + var ch = dv.chunks[i]; + if (ch.editFrom <= vpEdit.to && ch.editTo >= vpEdit.from && + ch.origFrom <= vpOrig.to && ch.origTo >= vpOrig.from) + drawConnectorsForChunk(dv, ch, sTopOrig, sTopEdit, w); + } + } + + function getMatchingOrigLine(editLine, chunks) { + var editStart = 0, origStart = 0; + for (var i = 0; i < chunks.length; i++) { + var chunk = chunks[i]; + if (chunk.editTo > editLine && chunk.editFrom <= editLine) return null; + if (chunk.editFrom > editLine) break; + editStart = chunk.editTo; + origStart = chunk.origTo; + } + return origStart + (editLine - editStart); + } + + // Combines information about chunks and widgets/markers to return + // an array of lines, in a single editor, that probably need to be + // aligned with their counterparts in the editor next to it. + function alignableFor(cm, chunks, isOrig) { + var tracker = cm.state.trackAlignable + var start = cm.firstLine(), trackI = 0 + var result = [] + for (var i = 0;; i++) { + var chunk = chunks[i] + var chunkStart = !chunk ? 1e9 : isOrig ? chunk.origFrom : chunk.editFrom + for (; trackI < tracker.alignable.length; trackI += 2) { + var n = tracker.alignable[trackI] + 1 + if (n <= start) continue + if (n <= chunkStart) result.push(n) + else break + } + if (!chunk) break + result.push(start = isOrig ? chunk.origTo : chunk.editTo) + } + return result + } + + // Given information about alignable lines in two editors, fill in + // the result (an array of three-element arrays) to reflect the + // lines that need to be aligned with each other. + function mergeAlignable(result, origAlignable, chunks, setIndex) { + var rI = 0, origI = 0, chunkI = 0, diff = 0 + outer: for (;; rI++) { + var nextR = result[rI], nextO = origAlignable[origI] + if (!nextR && nextO == null) break + + var rLine = nextR ? nextR[0] : 1e9, oLine = nextO == null ? 1e9 : nextO + while (chunkI < chunks.length) { + var chunk = chunks[chunkI] + if (chunk.origFrom <= oLine && chunk.origTo > oLine) { + origI++ + rI-- + continue outer; + } + if (chunk.editTo > rLine) { + if (chunk.editFrom <= rLine) continue outer; + break + } + diff += (chunk.origTo - chunk.origFrom) - (chunk.editTo - chunk.editFrom) + chunkI++ + } + if (rLine == oLine - diff) { + nextR[setIndex] = oLine + origI++ + } else if (rLine < oLine - diff) { + nextR[setIndex] = rLine + diff + } else { + var record = [oLine - diff, null, null] + record[setIndex] = oLine + result.splice(rI, 0, record) + origI++ + } + } + } + + function findAlignedLines(dv, other) { + var alignable = alignableFor(dv.edit, dv.chunks, false), result = [] + if (other) for (var i = 0, j = 0; i < other.chunks.length; i++) { + var n = other.chunks[i].editTo + while (j < alignable.length && alignable[j] < n) j++ + if (j == alignable.length || alignable[j] != n) alignable.splice(j++, 0, n) + } + for (var i = 0; i < alignable.length; i++) + result.push([alignable[i], null, null]) + + mergeAlignable(result, alignableFor(dv.orig, dv.chunks, true), dv.chunks, 1) + if (other) + mergeAlignable(result, alignableFor(other.orig, other.chunks, true), other.chunks, 2) + + return result + } + + function alignChunks(dv, force) { + if (!dv.dealigned && !force) return; + if (!dv.orig.curOp) return dv.orig.operation(function() { + alignChunks(dv, force); + }); + + dv.dealigned = false; + var other = dv.mv.left == dv ? dv.mv.right : dv.mv.left; + if (other) { + ensureDiff(other); + other.dealigned = false; + } + var linesToAlign = findAlignedLines(dv, other); + + // Clear old aligners + var aligners = dv.mv.aligners; + for (var i = 0; i < aligners.length; i++) + aligners[i].clear(); + aligners.length = 0; + + var cm = [dv.edit, dv.orig], scroll = []; + if (other) cm.push(other.orig); + for (var i = 0; i < cm.length; i++) + scroll.push(cm[i].getScrollInfo().top); + + for (var ln = 0; ln < linesToAlign.length; ln++) + alignLines(cm, linesToAlign[ln], aligners); + + for (var i = 0; i < cm.length; i++) + cm[i].scrollTo(null, scroll[i]); + } + + function alignLines(cm, lines, aligners) { + var maxOffset = 0, offset = []; + for (var i = 0; i < cm.length; i++) if (lines[i] != null) { + var off = cm[i].heightAtLine(lines[i], "local"); + offset[i] = off; + maxOffset = Math.max(maxOffset, off); + } + for (var i = 0; i < cm.length; i++) if (lines[i] != null) { + var diff = maxOffset - offset[i]; + if (diff > 1) + aligners.push(padAbove(cm[i], lines[i], diff)); + } + } + + function padAbove(cm, line, size) { + var above = true; + if (line > cm.lastLine()) { + line--; + above = false; + } + var elt = document.createElement("div"); + elt.className = "CodeMirror-merge-spacer"; + elt.style.height = size + "px"; elt.style.minWidth = "1px"; + return cm.addLineWidget(line, elt, {height: size, above: above, mergeSpacer: true, handleMouseEvents: true}); + } + + function drawConnectorsForChunk(dv, chunk, sTopOrig, sTopEdit, w) { + var flip = dv.type == "left"; + var top = dv.orig.heightAtLine(chunk.origFrom, "local", true) - sTopOrig; + if (dv.svg) { + var topLpx = top; + var topRpx = dv.edit.heightAtLine(chunk.editFrom, "local", true) - sTopEdit; + if (flip) { var tmp = topLpx; topLpx = topRpx; topRpx = tmp; } + var botLpx = dv.orig.heightAtLine(chunk.origTo, "local", true) - sTopOrig; + var botRpx = dv.edit.heightAtLine(chunk.editTo, "local", true) - sTopEdit; + if (flip) { var tmp = botLpx; botLpx = botRpx; botRpx = tmp; } + var curveTop = " C " + w/2 + " " + topRpx + " " + w/2 + " " + topLpx + " " + (w + 2) + " " + topLpx; + var curveBot = " C " + w/2 + " " + botLpx + " " + w/2 + " " + botRpx + " -1 " + botRpx; + attrs(dv.svg.appendChild(document.createElementNS(svgNS, "path")), + "d", "M -1 " + topRpx + curveTop + " L " + (w + 2) + " " + botLpx + curveBot + " z", + "class", dv.classes.connect); + } + if (dv.copyButtons) { + var copy = dv.copyButtons.appendChild(elt("div", dv.type == "left" ? "\u21dd" : "\u21dc", + "CodeMirror-merge-copy")); + var editOriginals = dv.mv.options.allowEditingOriginals; + copy.title = dv.edit.phrase(editOriginals ? "Push to left" : "Revert chunk"); + copy.chunk = chunk; + copy.style.top = (chunk.origTo > chunk.origFrom ? top : dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit) + "px"; + + if (editOriginals) { + var topReverse = dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit; + var copyReverse = dv.copyButtons.appendChild(elt("div", dv.type == "right" ? "\u21dd" : "\u21dc", + "CodeMirror-merge-copy-reverse")); + copyReverse.title = "Push to right"; + copyReverse.chunk = {editFrom: chunk.origFrom, editTo: chunk.origTo, + origFrom: chunk.editFrom, origTo: chunk.editTo}; + copyReverse.style.top = topReverse + "px"; + dv.type == "right" ? copyReverse.style.left = "2px" : copyReverse.style.right = "2px"; + } + } + } + + function copyChunk(dv, to, from, chunk) { + if (dv.diffOutOfDate) return; + var origStart = chunk.origTo > from.lastLine() ? Pos(chunk.origFrom - 1) : Pos(chunk.origFrom, 0) + var origEnd = Pos(chunk.origTo, 0) + var editStart = chunk.editTo > to.lastLine() ? Pos(chunk.editFrom - 1) : Pos(chunk.editFrom, 0) + var editEnd = Pos(chunk.editTo, 0) + var handler = dv.mv.options.revertChunk + if (handler) + handler(dv.mv, from, origStart, origEnd, to, editStart, editEnd) + else + to.replaceRange(from.getRange(origStart, origEnd), editStart, editEnd) + } + + // Merge view, containing 0, 1, or 2 diff views. + + var MergeView = CodeMirror.MergeView = function(node, options) { + if (!(this instanceof MergeView)) return new MergeView(node, options); + + this.options = options; + var origLeft = options.origLeft, origRight = options.origRight == null ? options.orig : options.origRight; + + var hasLeft = origLeft != null, hasRight = origRight != null; + var panes = 1 + (hasLeft ? 1 : 0) + (hasRight ? 1 : 0); + var wrap = [], left = this.left = null, right = this.right = null; + var self = this; + + if (hasLeft) { + left = this.left = new DiffView(this, "left"); + var leftPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-left"); + wrap.push(leftPane); + wrap.push(buildGap(left)); + } + + var editPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-editor"); + wrap.push(editPane); + + if (hasRight) { + right = this.right = new DiffView(this, "right"); + wrap.push(buildGap(right)); + var rightPane = elt("div", null, "CodeMirror-merge-pane CodeMirror-merge-right"); + wrap.push(rightPane); + } + + (hasRight ? rightPane : editPane).className += " CodeMirror-merge-pane-rightmost"; + + wrap.push(elt("div", null, null, "height: 0; clear: both;")); + + var wrapElt = this.wrap = node.appendChild(elt("div", wrap, "CodeMirror-merge CodeMirror-merge-" + panes + "pane")); + this.edit = CodeMirror(editPane, copyObj(options)); + + if (left) left.init(leftPane, origLeft, options); + if (right) right.init(rightPane, origRight, options); + if (options.collapseIdentical) + this.editor().operation(function() { + collapseIdenticalStretches(self, options.collapseIdentical); + }); + if (options.connect == "align") { + this.aligners = []; + alignChunks(this.left || this.right, true); + } + if (left) left.registerEvents(right) + if (right) right.registerEvents(left) + + + var onResize = function() { + if (left) makeConnections(left); + if (right) makeConnections(right); + }; + CodeMirror.on(window, "resize", onResize); + var resizeInterval = setInterval(function() { + for (var p = wrapElt.parentNode; p && p != document.body; p = p.parentNode) {} + if (!p) { clearInterval(resizeInterval); CodeMirror.off(window, "resize", onResize); } + }, 5000); + }; + + function buildGap(dv) { + var lock = dv.lockButton = elt("div", null, "CodeMirror-merge-scrolllock"); + var lockWrap = elt("div", [lock], "CodeMirror-merge-scrolllock-wrap"); + CodeMirror.on(lock, "click", function() { setScrollLock(dv, !dv.lockScroll); }); + var gapElts = [lockWrap]; + if (dv.mv.options.revertButtons !== false) { + dv.copyButtons = elt("div", null, "CodeMirror-merge-copybuttons-" + dv.type); + CodeMirror.on(dv.copyButtons, "click", function(e) { + var node = e.target || e.srcElement; + if (!node.chunk) return; + if (node.className == "CodeMirror-merge-copy-reverse") { + copyChunk(dv, dv.orig, dv.edit, node.chunk); + return; + } + copyChunk(dv, dv.edit, dv.orig, node.chunk); + }); + gapElts.unshift(dv.copyButtons); + } + if (dv.mv.options.connect != "align") { + var svg = document.createElementNS && document.createElementNS(svgNS, "svg"); + if (svg && !svg.createSVGRect) svg = null; + dv.svg = svg; + if (svg) gapElts.push(svg); + } + + return dv.gap = elt("div", gapElts, "CodeMirror-merge-gap"); + } + + MergeView.prototype = { + constructor: MergeView, + editor: function() { return this.edit; }, + rightOriginal: function() { return this.right && this.right.orig; }, + leftOriginal: function() { return this.left && this.left.orig; }, + setShowDifferences: function(val) { + if (this.right) this.right.setShowDifferences(val); + if (this.left) this.left.setShowDifferences(val); + }, + rightChunks: function() { + if (this.right) { ensureDiff(this.right); return this.right.chunks; } + }, + leftChunks: function() { + if (this.left) { ensureDiff(this.left); return this.left.chunks; } + } + }; + + function asString(obj) { + if (typeof obj == "string") return obj; + else return obj.getValue(); + } + + // Operations on diffs + var dmp; + function getDiff(a, b, ignoreWhitespace) { + if (!dmp) dmp = new diff_match_patch(); + + var diff = dmp.diff_main(a, b); + // The library sometimes leaves in empty parts, which confuse the algorithm + for (var i = 0; i < diff.length; ++i) { + var part = diff[i]; + if (ignoreWhitespace ? !/[^ \t]/.test(part[1]) : !part[1]) { + diff.splice(i--, 1); + } else if (i && diff[i - 1][0] == part[0]) { + diff.splice(i--, 1); + diff[i][1] += part[1]; + } + } + return diff; + } + + function getChunks(diff) { + var chunks = []; + if (!diff.length) return chunks; + var startEdit = 0, startOrig = 0; + var edit = Pos(0, 0), orig = Pos(0, 0); + for (var i = 0; i < diff.length; ++i) { + var part = diff[i], tp = part[0]; + if (tp == DIFF_EQUAL) { + var startOff = !startOfLineClean(diff, i) || edit.line < startEdit || orig.line < startOrig ? 1 : 0; + var cleanFromEdit = edit.line + startOff, cleanFromOrig = orig.line + startOff; + moveOver(edit, part[1], null, orig); + var endOff = endOfLineClean(diff, i) ? 1 : 0; + var cleanToEdit = edit.line + endOff, cleanToOrig = orig.line + endOff; + if (cleanToEdit > cleanFromEdit) { + if (i) chunks.push({origFrom: startOrig, origTo: cleanFromOrig, + editFrom: startEdit, editTo: cleanFromEdit}); + startEdit = cleanToEdit; startOrig = cleanToOrig; + } + } else { + moveOver(tp == DIFF_INSERT ? edit : orig, part[1]); + } + } + if (startEdit <= edit.line || startOrig <= orig.line) + chunks.push({origFrom: startOrig, origTo: orig.line + 1, + editFrom: startEdit, editTo: edit.line + 1}); + return chunks; + } + + function endOfLineClean(diff, i) { + if (i == diff.length - 1) return true; + var next = diff[i + 1][1]; + if ((next.length == 1 && i < diff.length - 2) || next.charCodeAt(0) != 10) return false; + if (i == diff.length - 2) return true; + next = diff[i + 2][1]; + return (next.length > 1 || i == diff.length - 3) && next.charCodeAt(0) == 10; + } + + function startOfLineClean(diff, i) { + if (i == 0) return true; + var last = diff[i - 1][1]; + if (last.charCodeAt(last.length - 1) != 10) return false; + if (i == 1) return true; + last = diff[i - 2][1]; + return last.charCodeAt(last.length - 1) == 10; + } + + function chunkBoundariesAround(chunks, n, nInEdit) { + var beforeE, afterE, beforeO, afterO; + for (var i = 0; i < chunks.length; i++) { + var chunk = chunks[i]; + var fromLocal = nInEdit ? chunk.editFrom : chunk.origFrom; + var toLocal = nInEdit ? chunk.editTo : chunk.origTo; + if (afterE == null) { + if (fromLocal > n) { afterE = chunk.editFrom; afterO = chunk.origFrom; } + else if (toLocal > n) { afterE = chunk.editTo; afterO = chunk.origTo; } + } + if (toLocal <= n) { beforeE = chunk.editTo; beforeO = chunk.origTo; } + else if (fromLocal <= n) { beforeE = chunk.editFrom; beforeO = chunk.origFrom; } + } + return {edit: {before: beforeE, after: afterE}, orig: {before: beforeO, after: afterO}}; + } + + function collapseSingle(cm, from, to) { + cm.addLineClass(from, "wrap", "CodeMirror-merge-collapsed-line"); + var widget = document.createElement("span"); + widget.className = "CodeMirror-merge-collapsed-widget"; + widget.title = cm.phrase("Identical text collapsed. Click to expand."); + var mark = cm.markText(Pos(from, 0), Pos(to - 1), { + inclusiveLeft: true, + inclusiveRight: true, + replacedWith: widget, + clearOnEnter: true + }); + function clear() { + mark.clear(); + cm.removeLineClass(from, "wrap", "CodeMirror-merge-collapsed-line"); + } + if (mark.explicitlyCleared) clear(); + CodeMirror.on(widget, "click", clear); + mark.on("clear", clear); + CodeMirror.on(widget, "click", clear); + return {mark: mark, clear: clear}; + } + + function collapseStretch(size, editors) { + var marks = []; + function clear() { + for (var i = 0; i < marks.length; i++) marks[i].clear(); + } + for (var i = 0; i < editors.length; i++) { + var editor = editors[i]; + var mark = collapseSingle(editor.cm, editor.line, editor.line + size); + marks.push(mark); + mark.mark.on("clear", clear); + } + return marks[0].mark; + } + + function unclearNearChunks(dv, margin, off, clear) { + for (var i = 0; i < dv.chunks.length; i++) { + var chunk = dv.chunks[i]; + for (var l = chunk.editFrom - margin; l < chunk.editTo + margin; l++) { + var pos = l + off; + if (pos >= 0 && pos < clear.length) clear[pos] = false; + } + } + } + + function collapseIdenticalStretches(mv, margin) { + if (typeof margin != "number") margin = 2; + var clear = [], edit = mv.editor(), off = edit.firstLine(); + for (var l = off, e = edit.lastLine(); l <= e; l++) clear.push(true); + if (mv.left) unclearNearChunks(mv.left, margin, off, clear); + if (mv.right) unclearNearChunks(mv.right, margin, off, clear); + + for (var i = 0; i < clear.length; i++) { + if (clear[i]) { + var line = i + off; + for (var size = 1; i < clear.length - 1 && clear[i + 1]; i++, size++) {} + if (size > margin) { + var editors = [{line: line, cm: edit}]; + if (mv.left) editors.push({line: getMatchingOrigLine(line, mv.left.chunks), cm: mv.left.orig}); + if (mv.right) editors.push({line: getMatchingOrigLine(line, mv.right.chunks), cm: mv.right.orig}); + var mark = collapseStretch(size, editors); + if (mv.options.onCollapse) mv.options.onCollapse(mv, line, size, mark); + } + } + } + } + + // General utilities + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + function clear(node) { + for (var count = node.childNodes.length; count > 0; --count) + node.removeChild(node.firstChild); + } + + function attrs(elt) { + for (var i = 1; i < arguments.length; i += 2) + elt.setAttribute(arguments[i], arguments[i+1]); + } + + function copyObj(obj, target) { + if (!target) target = {}; + for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop]; + return target; + } + + function moveOver(pos, str, copy, other) { + var out = copy ? Pos(pos.line, pos.ch) : pos, at = 0; + for (;;) { + var nl = str.indexOf("\n", at); + if (nl == -1) break; + ++out.line; + if (other) ++other.line; + at = nl + 1; + } + out.ch = (at ? 0 : out.ch) + (str.length - at); + if (other) other.ch = (at ? 0 : other.ch) + (str.length - at); + return out; + } + + // Tracks collapsed markers and line widgets, in order to be able to + // accurately align the content of two editors. + + var F_WIDGET = 1, F_WIDGET_BELOW = 2, F_MARKER = 4 + + function TrackAlignable(cm) { + this.cm = cm + this.alignable = [] + this.height = cm.doc.height + var self = this + cm.on("markerAdded", function(_, marker) { + if (!marker.collapsed) return + var found = marker.find(1) + if (found != null) self.set(found.line, F_MARKER) + }) + cm.on("markerCleared", function(_, marker, _min, max) { + if (max != null && marker.collapsed) + self.check(max, F_MARKER, self.hasMarker) + }) + cm.on("markerChanged", this.signal.bind(this)) + cm.on("lineWidgetAdded", function(_, widget, lineNo) { + if (widget.mergeSpacer) return + if (widget.above) self.set(lineNo - 1, F_WIDGET_BELOW) + else self.set(lineNo, F_WIDGET) + }) + cm.on("lineWidgetCleared", function(_, widget, lineNo) { + if (widget.mergeSpacer) return + if (widget.above) self.check(lineNo - 1, F_WIDGET_BELOW, self.hasWidgetBelow) + else self.check(lineNo, F_WIDGET, self.hasWidget) + }) + cm.on("lineWidgetChanged", this.signal.bind(this)) + cm.on("change", function(_, change) { + var start = change.from.line, nBefore = change.to.line - change.from.line + var nAfter = change.text.length - 1, end = start + nAfter + if (nBefore || nAfter) self.map(start, nBefore, nAfter) + self.check(end, F_MARKER, self.hasMarker) + if (nBefore || nAfter) self.check(change.from.line, F_MARKER, self.hasMarker) + }) + cm.on("viewportChange", function() { + if (self.cm.doc.height != self.height) self.signal() + }) + } + + TrackAlignable.prototype = { + signal: function() { + CodeMirror.signal(this, "realign") + this.height = this.cm.doc.height + }, + + set: function(n, flags) { + var pos = -1 + for (; pos < this.alignable.length; pos += 2) { + var diff = this.alignable[pos] - n + if (diff == 0) { + if ((this.alignable[pos + 1] & flags) == flags) return + this.alignable[pos + 1] |= flags + this.signal() + return + } + if (diff > 0) break + } + this.signal() + this.alignable.splice(pos, 0, n, flags) + }, + + find: function(n) { + for (var i = 0; i < this.alignable.length; i += 2) + if (this.alignable[i] == n) return i + return -1 + }, + + check: function(n, flag, pred) { + var found = this.find(n) + if (found == -1 || !(this.alignable[found + 1] & flag)) return + if (!pred.call(this, n)) { + this.signal() + var flags = this.alignable[found + 1] & ~flag + if (flags) this.alignable[found + 1] = flags + else this.alignable.splice(found, 2) + } + }, + + hasMarker: function(n) { + var handle = this.cm.getLineHandle(n) + if (handle.markedSpans) for (var i = 0; i < handle.markedSpans.length; i++) + if (handle.markedSpans[i].marker.collapsed && handle.markedSpans[i].to != null) + return true + return false + }, + + hasWidget: function(n) { + var handle = this.cm.getLineHandle(n) + if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++) + if (!handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true + return false + }, + + hasWidgetBelow: function(n) { + if (n == this.cm.lastLine()) return false + var handle = this.cm.getLineHandle(n + 1) + if (handle.widgets) for (var i = 0; i < handle.widgets.length; i++) + if (handle.widgets[i].above && !handle.widgets[i].mergeSpacer) return true + return false + }, + + map: function(from, nBefore, nAfter) { + var diff = nAfter - nBefore, to = from + nBefore, widgetFrom = -1, widgetTo = -1 + for (var i = 0; i < this.alignable.length; i += 2) { + var n = this.alignable[i] + if (n == from && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetFrom = i + if (n == to && (this.alignable[i + 1] & F_WIDGET_BELOW)) widgetTo = i + if (n <= from) continue + else if (n < to) this.alignable.splice(i--, 2) + else this.alignable[i] += diff + } + if (widgetFrom > -1) { + var flags = this.alignable[widgetFrom + 1] + if (flags == F_WIDGET_BELOW) this.alignable.splice(widgetFrom, 2) + else this.alignable[widgetFrom + 1] = flags & ~F_WIDGET_BELOW + } + if (widgetTo > -1 && nAfter) + this.set(from + nAfter, F_WIDGET_BELOW) + } + } + + function posMin(a, b) { return (a.line - b.line || a.ch - b.ch) < 0 ? a : b; } + function posMax(a, b) { return (a.line - b.line || a.ch - b.ch) > 0 ? a : b; } + function posEq(a, b) { return a.line == b.line && a.ch == b.ch; } + + function findPrevDiff(chunks, start, isOrig) { + for (var i = chunks.length - 1; i >= 0; i--) { + var chunk = chunks[i]; + var to = (isOrig ? chunk.origTo : chunk.editTo) - 1; + if (to < start) return to; + } + } + + function findNextDiff(chunks, start, isOrig) { + for (var i = 0; i < chunks.length; i++) { + var chunk = chunks[i]; + var from = (isOrig ? chunk.origFrom : chunk.editFrom); + if (from > start) return from; + } + } + + function goNearbyDiff(cm, dir) { + var found = null, views = cm.state.diffViews, line = cm.getCursor().line; + if (views) for (var i = 0; i < views.length; i++) { + var dv = views[i], isOrig = cm == dv.orig; + ensureDiff(dv); + var pos = dir < 0 ? findPrevDiff(dv.chunks, line, isOrig) : findNextDiff(dv.chunks, line, isOrig); + if (pos != null && (found == null || (dir < 0 ? pos > found : pos < found))) + found = pos; + } + if (found != null) + cm.setCursor(found, 0); + else + return CodeMirror.Pass; + } + + CodeMirror.commands.goNextDiff = function(cm) { + return goNearbyDiff(cm, 1); + }; + CodeMirror.commands.goPrevDiff = function(cm) { + return goNearbyDiff(cm, -1); + }; +}); diff --git a/public/vendor/plugins/codemirror/addon/mode/loadmode.js b/public/vendor/plugins/codemirror/addon/mode/loadmode.js index 10117ec22f..4ce716a012 100644 --- a/public/vendor/plugins/codemirror/addon/mode/loadmode.js +++ b/public/vendor/plugins/codemirror/addon/mode/loadmode.js @@ -1,5 +1,5 @@ // CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE +// Distributed under an MIT license: https://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS diff --git a/public/vendor/plugins/codemirror/addon/mode/multiplex.js b/public/vendor/plugins/codemirror/addon/mode/multiplex.js index 3d8b34c452..93fd9a5a46 100644 --- a/public/vendor/plugins/codemirror/addon/mode/multiplex.js +++ b/public/vendor/plugins/codemirror/addon/mode/multiplex.js @@ -1,5 +1,5 @@ // CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE +// Distributed under an MIT license: https://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS @@ -50,7 +50,15 @@ CodeMirror.multiplexingMode = function(outer /*, others */) { if (found == stream.pos) { if (!other.parseDelimiters) stream.match(other.open); state.innerActive = other; - state.inner = CodeMirror.startState(other.mode, outer.indent ? outer.indent(state.outer, "") : 0); + + // Get the outer indent, making sure to handle CodeMirror.Pass + var outerIndent = 0; + if (outer.indent) { + var possibleOuterIndent = outer.indent(state.outer, "", ""); + if (possibleOuterIndent !== CodeMirror.Pass) outerIndent = possibleOuterIndent; + } + + state.inner = CodeMirror.startState(other.mode, outerIndent); return other.delimStyle && (other.delimStyle + " " + other.delimStyle + "-open"); } else if (found != -1 && found < cutOff) { cutOff = found; @@ -88,10 +96,10 @@ CodeMirror.multiplexingMode = function(outer /*, others */) { } }, - indent: function(state, textAfter) { + indent: function(state, textAfter, line) { var mode = state.innerActive ? state.innerActive.mode : outer; if (!mode.indent) return CodeMirror.Pass; - return mode.indent(state.innerActive ? state.inner : state.outer, textAfter); + return mode.indent(state.innerActive ? state.inner : state.outer, textAfter, line); }, blankLine: function(state) { @@ -104,7 +112,7 @@ CodeMirror.multiplexingMode = function(outer /*, others */) { var other = others[i]; if (other.open === "\n") { state.innerActive = other; - state.inner = CodeMirror.startState(other.mode, mode.indent ? mode.indent(state.outer, "") : 0); + state.inner = CodeMirror.startState(other.mode, mode.indent ? mode.indent(state.outer, "", "") : 0); } } } else if (state.innerActive.close === "\n") { diff --git a/public/vendor/plugins/codemirror/addon/mode/multiplex_test.js b/public/vendor/plugins/codemirror/addon/mode/multiplex_test.js index 24e5e670de..c51cad45d5 100644 --- a/public/vendor/plugins/codemirror/addon/mode/multiplex_test.js +++ b/public/vendor/plugins/codemirror/addon/mode/multiplex_test.js @@ -1,5 +1,5 @@ // CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE +// Distributed under an MIT license: https://codemirror.net/LICENSE (function() { CodeMirror.defineMode("markdown_with_stex", function(){ diff --git a/public/vendor/plugins/codemirror/addon/mode/overlay.js b/public/vendor/plugins/codemirror/addon/mode/overlay.js index e1b9ed3753..016e3c28cc 100644 --- a/public/vendor/plugins/codemirror/addon/mode/overlay.js +++ b/public/vendor/plugins/codemirror/addon/mode/overlay.js @@ -1,5 +1,5 @@ // CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE +// Distributed under an MIT license: https://codemirror.net/LICENSE // Utility function that allows modes to be combined. The mode given // as the base argument takes care of most of the normal mode @@ -68,16 +68,21 @@ CodeMirror.overlayMode = function(base, overlay, combine) { else return state.overlayCur; }, - indent: base.indent && function(state, textAfter) { - return base.indent(state.base, textAfter); + indent: base.indent && function(state, textAfter, line) { + return base.indent(state.base, textAfter, line); }, electricChars: base.electricChars, innerMode: function(state) { return {state: state.base, mode: base}; }, blankLine: function(state) { - if (base.blankLine) base.blankLine(state.base); - if (overlay.blankLine) overlay.blankLine(state.overlay); + var baseToken, overlayToken; + if (base.blankLine) baseToken = base.blankLine(state.base); + if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay); + + return overlayToken == null ? + baseToken : + (combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken); } }; }; diff --git a/public/vendor/plugins/codemirror/addon/mode/simple.js b/public/vendor/plugins/codemirror/addon/mode/simple.js index df663365e8..655f991475 100644 --- a/public/vendor/plugins/codemirror/addon/mode/simple.js +++ b/public/vendor/plugins/codemirror/addon/mode/simple.js @@ -1,5 +1,5 @@ // CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE +// Distributed under an MIT license: https://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS @@ -77,6 +77,7 @@ function asToken(val) { if (!val) return null; + if (val.apply) return val if (typeof val == "string") return val.replace(/\./g, " "); var result = []; for (var i = 0; i < val.length; i++) @@ -133,17 +134,19 @@ state.indent.push(stream.indentation() + config.indentUnit); if (rule.data.dedent) state.indent.pop(); - if (matches.length > 2) { + var token = rule.token + if (token && token.apply) token = token(matches) + if (matches.length > 2 && rule.token && typeof rule.token != "string") { state.pending = []; for (var j = 2; j < matches.length; j++) if (matches[j]) state.pending.push({text: matches[j], token: rule.token[j - 1]}); stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0)); - return rule.token[0]; - } else if (rule.token && rule.token.join) { - return rule.token[0]; + return token[0]; + } else if (token && token.join) { + return token[0]; } else { - return rule.token; + return token; } } } diff --git a/public/vendor/plugins/codemirror/addon/runmode/colorize.js b/public/vendor/plugins/codemirror/addon/runmode/colorize.js new file mode 100644 index 0000000000..3be5411506 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/runmode/colorize.js @@ -0,0 +1,40 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./runmode")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./runmode"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var isBlock = /^(p|li|div|h\\d|pre|blockquote|td)$/; + + function textContent(node, out) { + if (node.nodeType == 3) return out.push(node.nodeValue); + for (var ch = node.firstChild; ch; ch = ch.nextSibling) { + textContent(ch, out); + if (isBlock.test(node.nodeType)) out.push("\n"); + } + } + + CodeMirror.colorize = function(collection, defaultMode) { + if (!collection) collection = document.body.getElementsByTagName("pre"); + + for (var i = 0; i < collection.length; ++i) { + var node = collection[i]; + var mode = node.getAttribute("data-lang") || defaultMode; + if (!mode) continue; + + var text = []; + textContent(node, text); + node.innerHTML = ""; + CodeMirror.runMode(text.join(""), mode, node); + + node.className += " cm-s-default"; + } + }; +}); diff --git a/public/vendor/plugins/codemirror/addon/runmode/runmode-standalone.js b/public/vendor/plugins/codemirror/addon/runmode/runmode-standalone.js new file mode 100644 index 0000000000..745eaf8440 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/runmode/runmode-standalone.js @@ -0,0 +1,158 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +window.CodeMirror = {}; + +(function() { +"use strict"; + +function splitLines(string){ return string.split(/\r?\n|\r/); }; + +function StringStream(string) { + this.pos = this.start = 0; + this.string = string; + this.lineStart = 0; +} +StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == 0;}, + peek: function() {return this.string.charAt(this.pos) || null;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() {return this.start - this.lineStart;}, + indentation: function() {return 0;}, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + }, + lookAhead: function() { return null } +}; +CodeMirror.StringStream = StringStream; + +CodeMirror.startState = function (mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; +}; + +var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; +CodeMirror.defineMode = function (name, mode) { + if (arguments.length > 2) + mode.dependencies = Array.prototype.slice.call(arguments, 2); + modes[name] = mode; +}; +CodeMirror.defineMIME = function (mime, spec) { mimeModes[mime] = spec; }; +CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + spec = mimeModes[spec.name]; + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; +}; +CodeMirror.getMode = function (options, spec) { + spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) throw new Error("Unknown mode: " + spec); + return mfactory(options, spec); +}; +CodeMirror.registerHelper = CodeMirror.registerGlobalHelper = Math.min; +CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; +}); +CodeMirror.defineMIME("text/plain", "null"); + +CodeMirror.runMode = function (string, modespec, callback, options) { + var mode = CodeMirror.getMode({ indentUnit: 2 }, modespec); + + if (callback.nodeType == 1) { + var tabSize = (options && options.tabSize) || 4; + var node = callback, col = 0; + node.innerHTML = ""; + callback = function (text, style) { + if (text == "\n") { + node.appendChild(document.createElement("br")); + col = 0; + return; + } + var content = ""; + // replace tabs + for (var pos = 0; ;) { + var idx = text.indexOf("\t", pos); + if (idx == -1) { + content += text.slice(pos); + col += text.length - pos; + break; + } else { + col += idx - pos; + content += text.slice(pos, idx); + var size = tabSize - col % tabSize; + col += size; + for (var i = 0; i < size; ++i) content += " "; + pos = idx + 1; + } + } + + if (style) { + var sp = node.appendChild(document.createElement("span")); + sp.className = "cm-" + style.replace(/ +/g, " cm-"); + sp.appendChild(document.createTextNode(content)); + } else { + node.appendChild(document.createTextNode(content)); + } + }; + } + + var lines = splitLines(string), state = (options && options.state) || CodeMirror.startState(mode); + for (var i = 0, e = lines.length; i < e; ++i) { + if (i) callback("\n"); + var stream = new CodeMirror.StringStream(lines[i]); + if (!stream.string && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + var style = mode.token(stream, state); + callback(stream.current(), style, i, stream.start, state); + stream.start = stream.pos; + } + } +}; +})(); diff --git a/public/vendor/plugins/codemirror/addon/runmode/runmode.js b/public/vendor/plugins/codemirror/addon/runmode/runmode.js new file mode 100644 index 0000000000..eb4cadf5b4 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/runmode/runmode.js @@ -0,0 +1,72 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.runMode = function(string, modespec, callback, options) { + var mode = CodeMirror.getMode(CodeMirror.defaults, modespec); + var ie = /MSIE \d/.test(navigator.userAgent); + var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9); + + if (callback.appendChild) { + var tabSize = (options && options.tabSize) || CodeMirror.defaults.tabSize; + var node = callback, col = 0; + node.innerHTML = ""; + callback = function(text, style) { + if (text == "\n") { + // Emitting LF or CRLF on IE8 or earlier results in an incorrect display. + // Emitting a carriage return makes everything ok. + node.appendChild(document.createTextNode(ie_lt9 ? '\r' : text)); + col = 0; + return; + } + var content = ""; + // replace tabs + for (var pos = 0;;) { + var idx = text.indexOf("\t", pos); + if (idx == -1) { + content += text.slice(pos); + col += text.length - pos; + break; + } else { + col += idx - pos; + content += text.slice(pos, idx); + var size = tabSize - col % tabSize; + col += size; + for (var i = 0; i < size; ++i) content += " "; + pos = idx + 1; + } + } + + if (style) { + var sp = node.appendChild(document.createElement("span")); + sp.className = "cm-" + style.replace(/ +/g, " cm-"); + sp.appendChild(document.createTextNode(content)); + } else { + node.appendChild(document.createTextNode(content)); + } + }; + } + + var lines = CodeMirror.splitLines(string), state = (options && options.state) || CodeMirror.startState(mode); + for (var i = 0, e = lines.length; i < e; ++i) { + if (i) callback("\n"); + var stream = new CodeMirror.StringStream(lines[i]); + if (!stream.string && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + var style = mode.token(stream, state); + callback(stream.current(), style, i, stream.start, state); + stream.start = stream.pos; + } + } +}; + +}); diff --git a/public/vendor/plugins/codemirror/addon/runmode/runmode.node.js b/public/vendor/plugins/codemirror/addon/runmode/runmode.node.js new file mode 100644 index 0000000000..53b6994c28 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/runmode/runmode.node.js @@ -0,0 +1,197 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +/* Just enough of CodeMirror to run runMode under node.js */ + +function splitLines(string){return string.split(/\r\n?|\n/);}; + +// Counts the column offset in a string, taking tabs into account. +// Used mostly to find indentation. +var countColumn = exports.countColumn = function(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } +}; + +function StringStream(string, tabSize, context) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + this.context = context +}; + +StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + }, + lookAhead: function(n) { + var line = this.context.line + n + return line >= this.context.lines.length ? null : this.context.lines[line] + } +}; +exports.StringStream = StringStream; + +exports.startState = function(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; +}; + +var modes = exports.modes = {}, mimeModes = exports.mimeModes = {}; +exports.defineMode = function(name, mode) { + if (arguments.length > 2) + mode.dependencies = Array.prototype.slice.call(arguments, 2); + modes[name] = mode; +}; +exports.defineMIME = function(mime, spec) { mimeModes[mime] = spec; }; + +exports.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; +}); +exports.defineMIME("text/plain", "null"); + +exports.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + spec = mimeModes[spec.name]; + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; +}; + +function copyObj(obj, target, overwrite) { + if (!target) target = {}; + for (var prop in obj) + if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + target[prop] = obj[prop]; + return target; +} + +// This can be used to attach properties to mode objects from +// outside the actual mode definition. +var modeExtensions = exports.modeExtensions = {}; +exports.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); +}; + +exports.getMode = function(options, spec) { + var spec = exports.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return exports.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; +}; + +exports.innerMode = function(mode, state) { + var info; + while (mode.innerMode) { + info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; +} + +exports.registerHelper = exports.registerGlobalHelper = Math.min; + +exports.runMode = function(string, modespec, callback, options) { + var mode = exports.getMode({indentUnit: 2}, modespec); + var lines = splitLines(string), state = (options && options.state) || exports.startState(mode); + var context = {lines: lines, line: 0} + for (var i = 0, e = lines.length; i < e; ++i, ++context.line) { + if (i) callback("\n"); + var stream = new exports.StringStream(lines[i], 4, context); + if (!stream.string && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + var style = mode.token(stream, state); + callback(stream.current(), style, i, stream.start, state); + stream.start = stream.pos; + } + } +}; + +require.cache[require.resolve("../../lib/codemirror")] = require.cache[require.resolve("./runmode.node")]; +require.cache[require.resolve("../../addon/runmode/runmode")] = require.cache[require.resolve("./runmode.node")]; diff --git a/public/vendor/plugins/codemirror/addon/scroll/annotatescrollbar.js b/public/vendor/plugins/codemirror/addon/scroll/annotatescrollbar.js new file mode 100644 index 0000000000..9fe61ec1ff --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/scroll/annotatescrollbar.js @@ -0,0 +1,122 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineExtension("annotateScrollbar", function(options) { + if (typeof options == "string") options = {className: options}; + return new Annotation(this, options); + }); + + CodeMirror.defineOption("scrollButtonHeight", 0); + + function Annotation(cm, options) { + this.cm = cm; + this.options = options; + this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight"); + this.annotations = []; + this.doRedraw = this.doUpdate = null; + this.div = cm.getWrapperElement().appendChild(document.createElement("div")); + this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none"; + this.computeScale(); + + function scheduleRedraw(delay) { + clearTimeout(self.doRedraw); + self.doRedraw = setTimeout(function() { self.redraw(); }, delay); + } + + var self = this; + cm.on("refresh", this.resizeHandler = function() { + clearTimeout(self.doUpdate); + self.doUpdate = setTimeout(function() { + if (self.computeScale()) scheduleRedraw(20); + }, 100); + }); + cm.on("markerAdded", this.resizeHandler); + cm.on("markerCleared", this.resizeHandler); + if (options.listenForChanges !== false) + cm.on("changes", this.changeHandler = function() { + scheduleRedraw(250); + }); + } + + Annotation.prototype.computeScale = function() { + var cm = this.cm; + var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) / + cm.getScrollerElement().scrollHeight + if (hScale != this.hScale) { + this.hScale = hScale; + return true; + } + }; + + Annotation.prototype.update = function(annotations) { + this.annotations = annotations; + this.redraw(); + }; + + Annotation.prototype.redraw = function(compute) { + if (compute !== false) this.computeScale(); + var cm = this.cm, hScale = this.hScale; + + var frag = document.createDocumentFragment(), anns = this.annotations; + + var wrapping = cm.getOption("lineWrapping"); + var singleLineH = wrapping && cm.defaultTextHeight() * 1.5; + var curLine = null, curLineObj = null; + function getY(pos, top) { + if (curLine != pos.line) { + curLine = pos.line; + curLineObj = cm.getLineHandle(curLine); + } + if ((curLineObj.widgets && curLineObj.widgets.length) || + (wrapping && curLineObj.height > singleLineH)) + return cm.charCoords(pos, "local")[top ? "top" : "bottom"]; + var topY = cm.heightAtLine(curLineObj, "local"); + return topY + (top ? 0 : curLineObj.height); + } + + var lastLine = cm.lastLine() + if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) { + var ann = anns[i]; + if (ann.to.line > lastLine) continue; + var top = nextTop || getY(ann.from, true) * hScale; + var bottom = getY(ann.to, false) * hScale; + while (i < anns.length - 1) { + if (anns[i + 1].to.line > lastLine) break; + nextTop = getY(anns[i + 1].from, true) * hScale; + if (nextTop > bottom + .9) break; + ann = anns[++i]; + bottom = getY(ann.to, false) * hScale; + } + if (bottom == top) continue; + var height = Math.max(bottom - top, 3); + + var elt = frag.appendChild(document.createElement("div")); + elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: " + + (top + this.buttonHeight) + "px; height: " + height + "px"; + elt.className = this.options.className; + if (ann.id) { + elt.setAttribute("annotation-id", ann.id); + } + } + this.div.textContent = ""; + this.div.appendChild(frag); + }; + + Annotation.prototype.clear = function() { + this.cm.off("refresh", this.resizeHandler); + this.cm.off("markerAdded", this.resizeHandler); + this.cm.off("markerCleared", this.resizeHandler); + if (this.changeHandler) this.cm.off("changes", this.changeHandler); + this.div.parentNode.removeChild(this.div); + }; +}); diff --git a/public/vendor/plugins/codemirror/addon/scroll/scrollpastend.js b/public/vendor/plugins/codemirror/addon/scroll/scrollpastend.js new file mode 100644 index 0000000000..2ed9d95e84 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/scroll/scrollpastend.js @@ -0,0 +1,48 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("scrollPastEnd", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.off("change", onChange); + cm.off("refresh", updateBottomMargin); + cm.display.lineSpace.parentNode.style.paddingBottom = ""; + cm.state.scrollPastEndPadding = null; + } + if (val) { + cm.on("change", onChange); + cm.on("refresh", updateBottomMargin); + updateBottomMargin(cm); + } + }); + + function onChange(cm, change) { + if (CodeMirror.changeEnd(change).line == cm.lastLine()) + updateBottomMargin(cm); + } + + function updateBottomMargin(cm) { + var padding = ""; + if (cm.lineCount() > 1) { + var totalH = cm.display.scroller.clientHeight - 30, + lastLineH = cm.getLineHandle(cm.lastLine()).height; + padding = (totalH - lastLineH) + "px"; + } + if (cm.state.scrollPastEndPadding != padding) { + cm.state.scrollPastEndPadding = padding; + cm.display.lineSpace.parentNode.style.paddingBottom = padding; + cm.off("refresh", updateBottomMargin); + cm.setSize(); + cm.on("refresh", updateBottomMargin); + } + } +}); diff --git a/public/vendor/plugins/codemirror/addon/scroll/simplescrollbars.css b/public/vendor/plugins/codemirror/addon/scroll/simplescrollbars.css new file mode 100644 index 0000000000..5eea7aa1b3 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/scroll/simplescrollbars.css @@ -0,0 +1,66 @@ +.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div { + position: absolute; + background: #ccc; + -moz-box-sizing: border-box; + box-sizing: border-box; + border: 1px solid #bbb; + border-radius: 2px; +} + +.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical { + position: absolute; + z-index: 6; + background: #eee; +} + +.CodeMirror-simplescroll-horizontal { + bottom: 0; left: 0; + height: 8px; +} +.CodeMirror-simplescroll-horizontal div { + bottom: 0; + height: 100%; +} + +.CodeMirror-simplescroll-vertical { + right: 0; top: 0; + width: 8px; +} +.CodeMirror-simplescroll-vertical div { + right: 0; + width: 100%; +} + + +.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler { + display: none; +} + +.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div { + position: absolute; + background: #bcd; + border-radius: 3px; +} + +.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical { + position: absolute; + z-index: 6; +} + +.CodeMirror-overlayscroll-horizontal { + bottom: 0; left: 0; + height: 6px; +} +.CodeMirror-overlayscroll-horizontal div { + bottom: 0; + height: 100%; +} + +.CodeMirror-overlayscroll-vertical { + right: 0; top: 0; + width: 6px; +} +.CodeMirror-overlayscroll-vertical div { + right: 0; + width: 100%; +} diff --git a/public/vendor/plugins/codemirror/addon/scroll/simplescrollbars.js b/public/vendor/plugins/codemirror/addon/scroll/simplescrollbars.js new file mode 100644 index 0000000000..750a2bd399 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/scroll/simplescrollbars.js @@ -0,0 +1,152 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function Bar(cls, orientation, scroll) { + this.orientation = orientation; + this.scroll = scroll; + this.screen = this.total = this.size = 1; + this.pos = 0; + + this.node = document.createElement("div"); + this.node.className = cls + "-" + orientation; + this.inner = this.node.appendChild(document.createElement("div")); + + var self = this; + CodeMirror.on(this.inner, "mousedown", function(e) { + if (e.which != 1) return; + CodeMirror.e_preventDefault(e); + var axis = self.orientation == "horizontal" ? "pageX" : "pageY"; + var start = e[axis], startpos = self.pos; + function done() { + CodeMirror.off(document, "mousemove", move); + CodeMirror.off(document, "mouseup", done); + } + function move(e) { + if (e.which != 1) return done(); + self.moveTo(startpos + (e[axis] - start) * (self.total / self.size)); + } + CodeMirror.on(document, "mousemove", move); + CodeMirror.on(document, "mouseup", done); + }); + + CodeMirror.on(this.node, "click", function(e) { + CodeMirror.e_preventDefault(e); + var innerBox = self.inner.getBoundingClientRect(), where; + if (self.orientation == "horizontal") + where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0; + else + where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0; + self.moveTo(self.pos + where * self.screen); + }); + + function onWheel(e) { + var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"]; + var oldPos = self.pos; + self.moveTo(self.pos + moved); + if (self.pos != oldPos) CodeMirror.e_preventDefault(e); + } + CodeMirror.on(this.node, "mousewheel", onWheel); + CodeMirror.on(this.node, "DOMMouseScroll", onWheel); + } + + Bar.prototype.setPos = function(pos, force) { + if (pos < 0) pos = 0; + if (pos > this.total - this.screen) pos = this.total - this.screen; + if (!force && pos == this.pos) return false; + this.pos = pos; + this.inner.style[this.orientation == "horizontal" ? "left" : "top"] = + (pos * (this.size / this.total)) + "px"; + return true + }; + + Bar.prototype.moveTo = function(pos) { + if (this.setPos(pos)) this.scroll(pos, this.orientation); + } + + var minButtonSize = 10; + + Bar.prototype.update = function(scrollSize, clientSize, barSize) { + var sizeChanged = this.screen != clientSize || this.total != scrollSize || this.size != barSize + if (sizeChanged) { + this.screen = clientSize; + this.total = scrollSize; + this.size = barSize; + } + + var buttonSize = this.screen * (this.size / this.total); + if (buttonSize < minButtonSize) { + this.size -= minButtonSize - buttonSize; + buttonSize = minButtonSize; + } + this.inner.style[this.orientation == "horizontal" ? "width" : "height"] = + buttonSize + "px"; + this.setPos(this.pos, sizeChanged); + }; + + function SimpleScrollbars(cls, place, scroll) { + this.addClass = cls; + this.horiz = new Bar(cls, "horizontal", scroll); + place(this.horiz.node); + this.vert = new Bar(cls, "vertical", scroll); + place(this.vert.node); + this.width = null; + } + + SimpleScrollbars.prototype.update = function(measure) { + if (this.width == null) { + var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle; + if (style) this.width = parseInt(style.height); + } + var width = this.width || 0; + + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + this.vert.node.style.display = needsV ? "block" : "none"; + this.horiz.node.style.display = needsH ? "block" : "none"; + + if (needsV) { + this.vert.update(measure.scrollHeight, measure.clientHeight, + measure.viewHeight - (needsH ? width : 0)); + this.vert.node.style.bottom = needsH ? width + "px" : "0"; + } + if (needsH) { + this.horiz.update(measure.scrollWidth, measure.clientWidth, + measure.viewWidth - (needsV ? width : 0) - measure.barLeft); + this.horiz.node.style.right = needsV ? width + "px" : "0"; + this.horiz.node.style.left = measure.barLeft + "px"; + } + + return {right: needsV ? width : 0, bottom: needsH ? width : 0}; + }; + + SimpleScrollbars.prototype.setScrollTop = function(pos) { + this.vert.setPos(pos); + }; + + SimpleScrollbars.prototype.setScrollLeft = function(pos) { + this.horiz.setPos(pos); + }; + + SimpleScrollbars.prototype.clear = function() { + var parent = this.horiz.node.parentNode; + parent.removeChild(this.horiz.node); + parent.removeChild(this.vert.node); + }; + + CodeMirror.scrollbarModel.simple = function(place, scroll) { + return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll); + }; + CodeMirror.scrollbarModel.overlay = function(place, scroll) { + return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll); + }; +}); diff --git a/public/vendor/plugins/codemirror/addon/search/jump-to-line.js b/public/vendor/plugins/codemirror/addon/search/jump-to-line.js new file mode 100644 index 0000000000..1f3526d247 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/search/jump-to-line.js @@ -0,0 +1,50 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Defines jumpToLine command. Uses dialog.js if present. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("../dialog/dialog")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "../dialog/dialog"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function dialog(cm, text, shortText, deflt, f) { + if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); + else f(prompt(shortText, deflt)); + } + + function getJumpDialog(cm) { + return cm.phrase("Jump to line:") + ' ' + cm.phrase("(Use line:column or scroll% syntax)") + ''; + } + + function interpretLine(cm, string) { + var num = Number(string) + if (/^[-+]/.test(string)) return cm.getCursor().line + num + else return num - 1 + } + + CodeMirror.commands.jumpToLine = function(cm) { + var cur = cm.getCursor(); + dialog(cm, getJumpDialog(cm), cm.phrase("Jump to line:"), (cur.line + 1) + ":" + cur.ch, function(posStr) { + if (!posStr) return; + + var match; + if (match = /^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(posStr)) { + cm.setCursor(interpretLine(cm, match[1]), Number(match[2])) + } else if (match = /^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(posStr)) { + var line = Math.round(cm.lineCount() * Number(match[1]) / 100); + if (/^[-+]/.test(match[1])) line = cur.line + line + 1; + cm.setCursor(line - 1, cur.ch); + } else if (match = /^\s*\:?\s*([\+\-]?\d+)\s*/.exec(posStr)) { + cm.setCursor(interpretLine(cm, match[1]), cur.ch); + } + }); + }; + + CodeMirror.keyMap["default"]["Alt-G"] = "jumpToLine"; +}); diff --git a/public/vendor/plugins/codemirror/addon/search/match-highlighter.js b/public/vendor/plugins/codemirror/addon/search/match-highlighter.js new file mode 100644 index 0000000000..b344ac79e2 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/search/match-highlighter.js @@ -0,0 +1,165 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Highlighting text that matches the selection +// +// Defines an option highlightSelectionMatches, which, when enabled, +// will style strings that match the selection throughout the +// document. +// +// The option can be set to true to simply enable it, or to a +// {minChars, style, wordsOnly, showToken, delay} object to explicitly +// configure it. minChars is the minimum amount of characters that should be +// selected for the behavior to occur, and style is the token style to +// apply to the matches. This will be prefixed by "cm-" to create an +// actual CSS class name. If wordsOnly is enabled, the matches will be +// highlighted only if the selected text is a word. showToken, when enabled, +// will cause the current token to be highlighted when nothing is selected. +// delay is used to specify how much time to wait, in milliseconds, before +// highlighting the matches. If annotateScrollbar is enabled, the occurences +// will be highlighted on the scrollbar via the matchesonscrollbar addon. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./matchesonscrollbar")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./matchesonscrollbar"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var defaults = { + style: "matchhighlight", + minChars: 2, + delay: 100, + wordsOnly: false, + annotateScrollbar: false, + showToken: false, + trim: true + } + + function State(options) { + this.options = {} + for (var name in defaults) + this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name] + this.overlay = this.timeout = null; + this.matchesonscroll = null; + this.active = false; + } + + CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + removeOverlay(cm); + clearTimeout(cm.state.matchHighlighter.timeout); + cm.state.matchHighlighter = null; + cm.off("cursorActivity", cursorActivity); + cm.off("focus", onFocus) + } + if (val) { + var state = cm.state.matchHighlighter = new State(val); + if (cm.hasFocus()) { + state.active = true + highlightMatches(cm) + } else { + cm.on("focus", onFocus) + } + cm.on("cursorActivity", cursorActivity); + } + }); + + function cursorActivity(cm) { + var state = cm.state.matchHighlighter; + if (state.active || cm.hasFocus()) scheduleHighlight(cm, state) + } + + function onFocus(cm) { + var state = cm.state.matchHighlighter + if (!state.active) { + state.active = true + scheduleHighlight(cm, state) + } + } + + function scheduleHighlight(cm, state) { + clearTimeout(state.timeout); + state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay); + } + + function addOverlay(cm, query, hasBoundary, style) { + var state = cm.state.matchHighlighter; + cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); + if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { + var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + "\\b") : query; + state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, + {className: "CodeMirror-selection-highlight-scrollbar"}); + } + } + + function removeOverlay(cm) { + var state = cm.state.matchHighlighter; + if (state.overlay) { + cm.removeOverlay(state.overlay); + state.overlay = null; + if (state.matchesonscroll) { + state.matchesonscroll.clear(); + state.matchesonscroll = null; + } + } + } + + function highlightMatches(cm) { + cm.operation(function() { + var state = cm.state.matchHighlighter; + removeOverlay(cm); + if (!cm.somethingSelected() && state.options.showToken) { + var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken; + var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start; + while (start && re.test(line.charAt(start - 1))) --start; + while (end < line.length && re.test(line.charAt(end))) ++end; + if (start < end) + addOverlay(cm, line.slice(start, end), re, state.options.style); + return; + } + var from = cm.getCursor("from"), to = cm.getCursor("to"); + if (from.line != to.line) return; + if (state.options.wordsOnly && !isWord(cm, from, to)) return; + var selection = cm.getRange(from, to) + if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "") + if (selection.length >= state.options.minChars) + addOverlay(cm, selection, false, state.options.style); + }); + } + + function isWord(cm, from, to) { + var str = cm.getRange(from, to); + if (str.match(/^\w+$/) !== null) { + if (from.ch > 0) { + var pos = {line: from.line, ch: from.ch - 1}; + var chr = cm.getRange(pos, from); + if (chr.match(/\W/) === null) return false; + } + if (to.ch < cm.getLine(from.line).length) { + var pos = {line: to.line, ch: to.ch + 1}; + var chr = cm.getRange(to, pos); + if (chr.match(/\W/) === null) return false; + } + return true; + } else return false; + } + + function boundariesAround(stream, re) { + return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) && + (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos))); + } + + function makeOverlay(query, hasBoundary, style) { + return {token: function(stream) { + if (stream.match(query) && + (!hasBoundary || boundariesAround(stream, hasBoundary))) + return style; + stream.next(); + stream.skipTo(query.charAt(0)) || stream.skipToEnd(); + }}; + } +}); diff --git a/public/vendor/plugins/codemirror/addon/search/matchesonscrollbar.css b/public/vendor/plugins/codemirror/addon/search/matchesonscrollbar.css new file mode 100644 index 0000000000..77932cc908 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/search/matchesonscrollbar.css @@ -0,0 +1,8 @@ +.CodeMirror-search-match { + background: gold; + border-top: 1px solid orange; + border-bottom: 1px solid orange; + -moz-box-sizing: border-box; + box-sizing: border-box; + opacity: .5; +} diff --git a/public/vendor/plugins/codemirror/addon/search/matchesonscrollbar.js b/public/vendor/plugins/codemirror/addon/search/matchesonscrollbar.js new file mode 100644 index 0000000000..8a4a827584 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/search/matchesonscrollbar.js @@ -0,0 +1,97 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) { + if (typeof options == "string") options = {className: options}; + if (!options) options = {}; + return new SearchAnnotation(this, query, caseFold, options); + }); + + function SearchAnnotation(cm, query, caseFold, options) { + this.cm = cm; + this.options = options; + var annotateOptions = {listenForChanges: false}; + for (var prop in options) annotateOptions[prop] = options[prop]; + if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match"; + this.annotation = cm.annotateScrollbar(annotateOptions); + this.query = query; + this.caseFold = caseFold; + this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1}; + this.matches = []; + this.update = null; + + this.findMatches(); + this.annotation.update(this.matches); + + var self = this; + cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); }); + } + + var MAX_MATCHES = 1000; + + SearchAnnotation.prototype.findMatches = function() { + if (!this.gap) return; + for (var i = 0; i < this.matches.length; i++) { + var match = this.matches[i]; + if (match.from.line >= this.gap.to) break; + if (match.to.line >= this.gap.from) this.matches.splice(i--, 1); + } + var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), {caseFold: this.caseFold, multiline: this.options.multiline}); + var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES; + while (cursor.findNext()) { + var match = {from: cursor.from(), to: cursor.to()}; + if (match.from.line >= this.gap.to) break; + this.matches.splice(i++, 0, match); + if (this.matches.length > maxMatches) break; + } + this.gap = null; + }; + + function offsetLine(line, changeStart, sizeChange) { + if (line <= changeStart) return line; + return Math.max(changeStart, line + sizeChange); + } + + SearchAnnotation.prototype.onChange = function(change) { + var startLine = change.from.line; + var endLine = CodeMirror.changeEnd(change).line; + var sizeChange = endLine - change.to.line; + if (this.gap) { + this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line); + this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line); + } else { + this.gap = {from: change.from.line, to: endLine + 1}; + } + + if (sizeChange) for (var i = 0; i < this.matches.length; i++) { + var match = this.matches[i]; + var newFrom = offsetLine(match.from.line, startLine, sizeChange); + if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch); + var newTo = offsetLine(match.to.line, startLine, sizeChange); + if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch); + } + clearTimeout(this.update); + var self = this; + this.update = setTimeout(function() { self.updateAfterChange(); }, 250); + }; + + SearchAnnotation.prototype.updateAfterChange = function() { + this.findMatches(); + this.annotation.update(this.matches); + }; + + SearchAnnotation.prototype.clear = function() { + this.cm.off("change", this.changeHandler); + this.annotation.clear(); + }; +}); diff --git a/public/vendor/plugins/codemirror/addon/search/search.js b/public/vendor/plugins/codemirror/addon/search/search.js new file mode 100644 index 0000000000..cecdd52ea1 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/search/search.js @@ -0,0 +1,260 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Define search commands. Depends on dialog.js or another +// implementation of the openDialog method. + +// Replace works a little oddly -- it will do the replace on the next +// Ctrl-G (or whatever is bound to findNext) press. You prevent a +// replace by making sure the match is no longer selected when hitting +// Ctrl-G. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + function searchOverlay(query, caseInsensitive) { + if (typeof query == "string") + query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g"); + else if (!query.global) + query = new RegExp(query.source, query.ignoreCase ? "gi" : "g"); + + return {token: function(stream) { + query.lastIndex = stream.pos; + var match = query.exec(stream.string); + if (match && match.index == stream.pos) { + stream.pos += match[0].length || 1; + return "searching"; + } else if (match) { + stream.pos = match.index; + } else { + stream.skipToEnd(); + } + }}; + } + + function SearchState() { + this.posFrom = this.posTo = this.lastQuery = this.query = null; + this.overlay = null; + } + + function getSearchState(cm) { + return cm.state.search || (cm.state.search = new SearchState()); + } + + function queryCaseInsensitive(query) { + return typeof query == "string" && query == query.toLowerCase(); + } + + function getSearchCursor(cm, query, pos) { + // Heuristic: if the query string is all lowercase, do a case insensitive search. + return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true}); + } + + function persistentDialog(cm, text, deflt, onEnter, onKeyDown) { + cm.openDialog(text, onEnter, { + value: deflt, + selectValueOnOpen: true, + closeOnEnter: false, + onClose: function() { clearSearch(cm); }, + onKeyDown: onKeyDown + }); + } + + function dialog(cm, text, shortText, deflt, f) { + if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true}); + else f(prompt(shortText, deflt)); + } + + function confirmDialog(cm, text, shortText, fs) { + if (cm.openConfirm) cm.openConfirm(text, fs); + else if (confirm(shortText)) fs[0](); + } + + function parseString(string) { + return string.replace(/\\([nrt\\])/g, function(match, ch) { + if (ch == "n") return "\n" + if (ch == "r") return "\r" + if (ch == "t") return "\t" + if (ch == "\\") return "\\" + return match + }) + } + + function parseQuery(query) { + var isRE = query.match(/^\/(.*)\/([a-z]*)$/); + if (isRE) { + try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); } + catch(e) {} // Not a regular expression after all, do a string search + } else { + query = parseString(query) + } + if (typeof query == "string" ? query == "" : query.test("")) + query = /x^/; + return query; + } + + function startSearch(cm, state, query) { + state.queryText = query; + state.query = parseQuery(query); + cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); + state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); + cm.addOverlay(state.overlay); + if (cm.showMatchesOnScrollbar) { + if (state.annotate) { state.annotate.clear(); state.annotate = null; } + state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query)); + } + } + + function doSearch(cm, rev, persistent, immediate) { + var state = getSearchState(cm); + if (state.query) return findNext(cm, rev); + var q = cm.getSelection() || state.lastQuery; + if (q instanceof RegExp && q.source == "x^") q = null + if (persistent && cm.openDialog) { + var hiding = null + var searchNext = function(query, event) { + CodeMirror.e_stop(event); + if (!query) return; + if (query != state.queryText) { + startSearch(cm, state, query); + state.posFrom = state.posTo = cm.getCursor(); + } + if (hiding) hiding.style.opacity = 1 + findNext(cm, event.shiftKey, function(_, to) { + var dialog + if (to.line < 3 && document.querySelector && + (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) && + dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top) + (hiding = dialog).style.opacity = .4 + }) + }; + persistentDialog(cm, getQueryDialog(cm), q, searchNext, function(event, query) { + var keyName = CodeMirror.keyName(event) + var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName] + if (cmd == "findNext" || cmd == "findPrev" || + cmd == "findPersistentNext" || cmd == "findPersistentPrev") { + CodeMirror.e_stop(event); + startSearch(cm, getSearchState(cm), query); + cm.execCommand(cmd); + } else if (cmd == "find" || cmd == "findPersistent") { + CodeMirror.e_stop(event); + searchNext(query, event); + } + }); + if (immediate && q) { + startSearch(cm, state, q); + findNext(cm, rev); + } + } else { + dialog(cm, getQueryDialog(cm), "Search for:", q, function(query) { + if (query && !state.query) cm.operation(function() { + startSearch(cm, state, query); + state.posFrom = state.posTo = cm.getCursor(); + findNext(cm, rev); + }); + }); + } + } + + function findNext(cm, rev, callback) {cm.operation(function() { + var state = getSearchState(cm); + var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); + if (!cursor.find(rev)) { + cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)); + if (!cursor.find(rev)) return; + } + cm.setSelection(cursor.from(), cursor.to()); + cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20); + state.posFrom = cursor.from(); state.posTo = cursor.to(); + if (callback) callback(cursor.from(), cursor.to()) + });} + + function clearSearch(cm) {cm.operation(function() { + var state = getSearchState(cm); + state.lastQuery = state.query; + if (!state.query) return; + state.query = state.queryText = null; + cm.removeOverlay(state.overlay); + if (state.annotate) { state.annotate.clear(); state.annotate = null; } + });} + + + function getQueryDialog(cm) { + return '' + cm.phrase("Search:") + ' ' + cm.phrase("(Use /re/ syntax for regexp search)") + ''; + } + function getReplaceQueryDialog(cm) { + return ' ' + cm.phrase("(Use /re/ syntax for regexp search)") + ''; + } + function getReplacementQueryDialog(cm) { + return '' + cm.phrase("With:") + ' '; + } + function getDoReplaceConfirm(cm) { + return '' + cm.phrase("Replace?") + ' '; + } + + function replaceAll(cm, query, text) { + cm.operation(function() { + for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { + if (typeof query != "string") { + var match = cm.getRange(cursor.from(), cursor.to()).match(query); + cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); + } else cursor.replace(text); + } + }); + } + + function replace(cm, all) { + if (cm.getOption("readOnly")) return; + var query = cm.getSelection() || getSearchState(cm).lastQuery; + var dialogText = '' + (all ? cm.phrase("Replace all:") : cm.phrase("Replace:")) + ''; + dialog(cm, dialogText + getReplaceQueryDialog(cm), dialogText, query, function(query) { + if (!query) return; + query = parseQuery(query); + dialog(cm, getReplacementQueryDialog(cm), cm.phrase("Replace with:"), "", function(text) { + text = parseString(text) + if (all) { + replaceAll(cm, query, text) + } else { + clearSearch(cm); + var cursor = getSearchCursor(cm, query, cm.getCursor("from")); + var advance = function() { + var start = cursor.from(), match; + if (!(match = cursor.findNext())) { + cursor = getSearchCursor(cm, query); + if (!(match = cursor.findNext()) || + (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; + } + cm.setSelection(cursor.from(), cursor.to()); + cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); + confirmDialog(cm, getDoReplaceConfirm(cm), cm.phrase("Replace?"), + [function() {doReplace(match);}, advance, + function() {replaceAll(cm, query, text)}]); + }; + var doReplace = function(match) { + cursor.replace(typeof query == "string" ? text : + text.replace(/\$(\d)/g, function(_, i) {return match[i];})); + advance(); + }; + advance(); + } + }); + }); + } + + CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);}; + CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);}; + CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);}; + CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);}; + CodeMirror.commands.findNext = doSearch; + CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);}; + CodeMirror.commands.clearSearch = clearSearch; + CodeMirror.commands.replace = replace; + CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);}; +}); diff --git a/public/vendor/plugins/codemirror/addon/search/searchcursor.js b/public/vendor/plugins/codemirror/addon/search/searchcursor.js new file mode 100644 index 0000000000..aae36dfe53 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/search/searchcursor.js @@ -0,0 +1,293 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")) + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod) + else // Plain browser env + mod(CodeMirror) +})(function(CodeMirror) { + "use strict" + var Pos = CodeMirror.Pos + + function regexpFlags(regexp) { + var flags = regexp.flags + return flags != null ? flags : (regexp.ignoreCase ? "i" : "") + + (regexp.global ? "g" : "") + + (regexp.multiline ? "m" : "") + } + + function ensureFlags(regexp, flags) { + var current = regexpFlags(regexp), target = current + for (var i = 0; i < flags.length; i++) if (target.indexOf(flags.charAt(i)) == -1) + target += flags.charAt(i) + return current == target ? regexp : new RegExp(regexp.source, target) + } + + function maybeMultiline(regexp) { + return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source) + } + + function searchRegexpForward(doc, regexp, start) { + regexp = ensureFlags(regexp, "g") + for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) { + regexp.lastIndex = ch + var string = doc.getLine(line), match = regexp.exec(string) + if (match) + return {from: Pos(line, match.index), + to: Pos(line, match.index + match[0].length), + match: match} + } + } + + function searchRegexpForwardMultiline(doc, regexp, start) { + if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start) + + regexp = ensureFlags(regexp, "gm") + var string, chunk = 1 + for (var line = start.line, last = doc.lastLine(); line <= last;) { + // This grows the search buffer in exponentially-sized chunks + // between matches, so that nearby matches are fast and don't + // require concatenating the whole document (in case we're + // searching for something that has tons of matches), but at the + // same time, the amount of retries is limited. + for (var i = 0; i < chunk; i++) { + if (line > last) break + var curLine = doc.getLine(line++) + string = string == null ? curLine : string + "\n" + curLine + } + chunk = chunk * 2 + regexp.lastIndex = start.ch + var match = regexp.exec(string) + if (match) { + var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") + var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length + return {from: Pos(startLine, startCh), + to: Pos(startLine + inside.length - 1, + inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), + match: match} + } + } + } + + function lastMatchIn(string, regexp) { + var cutOff = 0, match + for (;;) { + regexp.lastIndex = cutOff + var newMatch = regexp.exec(string) + if (!newMatch) return match + match = newMatch + cutOff = match.index + (match[0].length || 1) + if (cutOff == string.length) return match + } + } + + function searchRegexpBackward(doc, regexp, start) { + regexp = ensureFlags(regexp, "g") + for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) { + var string = doc.getLine(line) + if (ch > -1) string = string.slice(0, ch) + var match = lastMatchIn(string, regexp) + if (match) + return {from: Pos(line, match.index), + to: Pos(line, match.index + match[0].length), + match: match} + } + } + + function searchRegexpBackwardMultiline(doc, regexp, start) { + regexp = ensureFlags(regexp, "gm") + var string, chunk = 1 + for (var line = start.line, first = doc.firstLine(); line >= first;) { + for (var i = 0; i < chunk; i++) { + var curLine = doc.getLine(line--) + string = string == null ? curLine.slice(0, start.ch) : curLine + "\n" + string + } + chunk *= 2 + + var match = lastMatchIn(string, regexp) + if (match) { + var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n") + var startLine = line + before.length, startCh = before[before.length - 1].length + return {from: Pos(startLine, startCh), + to: Pos(startLine + inside.length - 1, + inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length), + match: match} + } + } + } + + var doFold, noFold + if (String.prototype.normalize) { + doFold = function(str) { return str.normalize("NFD").toLowerCase() } + noFold = function(str) { return str.normalize("NFD") } + } else { + doFold = function(str) { return str.toLowerCase() } + noFold = function(str) { return str } + } + + // Maps a position in a case-folded line back to a position in the original line + // (compensating for codepoints increasing in number during folding) + function adjustPos(orig, folded, pos, foldFunc) { + if (orig.length == folded.length) return pos + for (var min = 0, max = pos + Math.max(0, orig.length - folded.length);;) { + if (min == max) return min + var mid = (min + max) >> 1 + var len = foldFunc(orig.slice(0, mid)).length + if (len == pos) return mid + else if (len > pos) max = mid + else min = mid + 1 + } + } + + function searchStringForward(doc, query, start, caseFold) { + // Empty string would match anything and never progress, so we + // define it to match nothing instead. + if (!query.length) return null + var fold = caseFold ? doFold : noFold + var lines = fold(query).split(/\r|\n\r?/) + + search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) { + var orig = doc.getLine(line).slice(ch), string = fold(orig) + if (lines.length == 1) { + var found = string.indexOf(lines[0]) + if (found == -1) continue search + var start = adjustPos(orig, string, found, fold) + ch + return {from: Pos(line, adjustPos(orig, string, found, fold) + ch), + to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)} + } else { + var cutFrom = string.length - lines[0].length + if (string.slice(cutFrom) != lines[0]) continue search + for (var i = 1; i < lines.length - 1; i++) + if (fold(doc.getLine(line + i)) != lines[i]) continue search + var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1] + if (endString.slice(0, lastLine.length) != lastLine) continue search + return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch), + to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))} + } + } + } + + function searchStringBackward(doc, query, start, caseFold) { + if (!query.length) return null + var fold = caseFold ? doFold : noFold + var lines = fold(query).split(/\r|\n\r?/) + + search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) { + var orig = doc.getLine(line) + if (ch > -1) orig = orig.slice(0, ch) + var string = fold(orig) + if (lines.length == 1) { + var found = string.lastIndexOf(lines[0]) + if (found == -1) continue search + return {from: Pos(line, adjustPos(orig, string, found, fold)), + to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))} + } else { + var lastLine = lines[lines.length - 1] + if (string.slice(0, lastLine.length) != lastLine) continue search + for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++) + if (fold(doc.getLine(start + i)) != lines[i]) continue search + var top = doc.getLine(line + 1 - lines.length), topString = fold(top) + if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search + return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)), + to: Pos(line, adjustPos(orig, string, lastLine.length, fold))} + } + } + } + + function SearchCursor(doc, query, pos, options) { + this.atOccurrence = false + this.doc = doc + pos = pos ? doc.clipPos(pos) : Pos(0, 0) + this.pos = {from: pos, to: pos} + + var caseFold + if (typeof options == "object") { + caseFold = options.caseFold + } else { // Backwards compat for when caseFold was the 4th argument + caseFold = options + options = null + } + + if (typeof query == "string") { + if (caseFold == null) caseFold = false + this.matches = function(reverse, pos) { + return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold) + } + } else { + query = ensureFlags(query, "gm") + if (!options || options.multiline !== false) + this.matches = function(reverse, pos) { + return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos) + } + else + this.matches = function(reverse, pos) { + return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos) + } + } + } + + SearchCursor.prototype = { + findNext: function() {return this.find(false)}, + findPrevious: function() {return this.find(true)}, + + find: function(reverse) { + var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to)) + + // Implements weird auto-growing behavior on null-matches for + // backwards-compatiblity with the vim code (unfortunately) + while (result && CodeMirror.cmpPos(result.from, result.to) == 0) { + if (reverse) { + if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1) + else if (result.from.line == this.doc.firstLine()) result = null + else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1))) + } else { + if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1) + else if (result.to.line == this.doc.lastLine()) result = null + else result = this.matches(reverse, Pos(result.to.line + 1, 0)) + } + } + + if (result) { + this.pos = result + this.atOccurrence = true + return this.pos.match || true + } else { + var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0) + this.pos = {from: end, to: end} + return this.atOccurrence = false + } + }, + + from: function() {if (this.atOccurrence) return this.pos.from}, + to: function() {if (this.atOccurrence) return this.pos.to}, + + replace: function(newText, origin) { + if (!this.atOccurrence) return + var lines = CodeMirror.splitLines(newText) + this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin) + this.pos.to = Pos(this.pos.from.line + lines.length - 1, + lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0)) + } + } + + CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) { + return new SearchCursor(this.doc, query, pos, caseFold) + }) + CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) { + return new SearchCursor(this, query, pos, caseFold) + }) + + CodeMirror.defineExtension("selectMatches", function(query, caseFold) { + var ranges = [] + var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold) + while (cur.findNext()) { + if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break + ranges.push({anchor: cur.from(), head: cur.to()}) + } + if (ranges.length) + this.setSelections(ranges, 0) + }) +}); diff --git a/public/vendor/plugins/codemirror/addon/selection/active-line.js b/public/vendor/plugins/codemirror/addon/selection/active-line.js new file mode 100644 index 0000000000..c7b14ce07f --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/selection/active-line.js @@ -0,0 +1,72 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + var WRAP_CLASS = "CodeMirror-activeline"; + var BACK_CLASS = "CodeMirror-activeline-background"; + var GUTT_CLASS = "CodeMirror-activeline-gutter"; + + CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) { + var prev = old == CodeMirror.Init ? false : old; + if (val == prev) return + if (prev) { + cm.off("beforeSelectionChange", selectionChange); + clearActiveLines(cm); + delete cm.state.activeLines; + } + if (val) { + cm.state.activeLines = []; + updateActiveLines(cm, cm.listSelections()); + cm.on("beforeSelectionChange", selectionChange); + } + }); + + function clearActiveLines(cm) { + for (var i = 0; i < cm.state.activeLines.length; i++) { + cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS); + cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS); + cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS); + } + } + + function sameArray(a, b) { + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) + if (a[i] != b[i]) return false; + return true; + } + + function updateActiveLines(cm, ranges) { + var active = []; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + var option = cm.getOption("styleActiveLine"); + if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty()) + continue + var line = cm.getLineHandleVisualStart(range.head.line); + if (active[active.length - 1] != line) active.push(line); + } + if (sameArray(cm.state.activeLines, active)) return; + cm.operation(function() { + clearActiveLines(cm); + for (var i = 0; i < active.length; i++) { + cm.addLineClass(active[i], "wrap", WRAP_CLASS); + cm.addLineClass(active[i], "background", BACK_CLASS); + cm.addLineClass(active[i], "gutter", GUTT_CLASS); + } + cm.state.activeLines = active; + }); + } + + function selectionChange(cm, sel) { + updateActiveLines(cm, sel.ranges); + } +}); diff --git a/public/vendor/plugins/codemirror/addon/selection/mark-selection.js b/public/vendor/plugins/codemirror/addon/selection/mark-selection.js new file mode 100644 index 0000000000..adfaa62d1a --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/selection/mark-selection.js @@ -0,0 +1,119 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Because sometimes you need to mark the selected *text*. +// +// Adds an option 'styleSelectedText' which, when enabled, gives +// selected text the CSS class given as option value, or +// "CodeMirror-selectedtext" when the value is not a string. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("styleSelectedText", false, function(cm, val, old) { + var prev = old && old != CodeMirror.Init; + if (val && !prev) { + cm.state.markedSelection = []; + cm.state.markedSelectionStyle = typeof val == "string" ? val : "CodeMirror-selectedtext"; + reset(cm); + cm.on("cursorActivity", onCursorActivity); + cm.on("change", onChange); + } else if (!val && prev) { + cm.off("cursorActivity", onCursorActivity); + cm.off("change", onChange); + clear(cm); + cm.state.markedSelection = cm.state.markedSelectionStyle = null; + } + }); + + function onCursorActivity(cm) { + if (cm.state.markedSelection) + cm.operation(function() { update(cm); }); + } + + function onChange(cm) { + if (cm.state.markedSelection && cm.state.markedSelection.length) + cm.operation(function() { clear(cm); }); + } + + var CHUNK_SIZE = 8; + var Pos = CodeMirror.Pos; + var cmp = CodeMirror.cmpPos; + + function coverRange(cm, from, to, addAt) { + if (cmp(from, to) == 0) return; + var array = cm.state.markedSelection; + var cls = cm.state.markedSelectionStyle; + for (var line = from.line;;) { + var start = line == from.line ? from : Pos(line, 0); + var endLine = line + CHUNK_SIZE, atEnd = endLine >= to.line; + var end = atEnd ? to : Pos(endLine, 0); + var mark = cm.markText(start, end, {className: cls}); + if (addAt == null) array.push(mark); + else array.splice(addAt++, 0, mark); + if (atEnd) break; + line = endLine; + } + } + + function clear(cm) { + var array = cm.state.markedSelection; + for (var i = 0; i < array.length; ++i) array[i].clear(); + array.length = 0; + } + + function reset(cm) { + clear(cm); + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) + coverRange(cm, ranges[i].from(), ranges[i].to()); + } + + function update(cm) { + if (!cm.somethingSelected()) return clear(cm); + if (cm.listSelections().length > 1) return reset(cm); + + var from = cm.getCursor("start"), to = cm.getCursor("end"); + + var array = cm.state.markedSelection; + if (!array.length) return coverRange(cm, from, to); + + var coverStart = array[0].find(), coverEnd = array[array.length - 1].find(); + if (!coverStart || !coverEnd || to.line - from.line <= CHUNK_SIZE || + cmp(from, coverEnd.to) >= 0 || cmp(to, coverStart.from) <= 0) + return reset(cm); + + while (cmp(from, coverStart.from) > 0) { + array.shift().clear(); + coverStart = array[0].find(); + } + if (cmp(from, coverStart.from) < 0) { + if (coverStart.to.line - from.line < CHUNK_SIZE) { + array.shift().clear(); + coverRange(cm, from, coverStart.to, 0); + } else { + coverRange(cm, from, coverStart.from, 0); + } + } + + while (cmp(to, coverEnd.to) < 0) { + array.pop().clear(); + coverEnd = array[array.length - 1].find(); + } + if (cmp(to, coverEnd.to) > 0) { + if (to.line - coverEnd.from.line < CHUNK_SIZE) { + array.pop().clear(); + coverRange(cm, coverEnd.from, to); + } else { + coverRange(cm, coverEnd.to, to); + } + } + } +}); diff --git a/public/vendor/plugins/codemirror/addon/selection/selection-pointer.js b/public/vendor/plugins/codemirror/addon/selection/selection-pointer.js new file mode 100644 index 0000000000..f0bd61a33e --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/selection/selection-pointer.js @@ -0,0 +1,98 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + CodeMirror.defineOption("selectionPointer", false, function(cm, val) { + var data = cm.state.selectionPointer; + if (data) { + CodeMirror.off(cm.getWrapperElement(), "mousemove", data.mousemove); + CodeMirror.off(cm.getWrapperElement(), "mouseout", data.mouseout); + CodeMirror.off(window, "scroll", data.windowScroll); + cm.off("cursorActivity", reset); + cm.off("scroll", reset); + cm.state.selectionPointer = null; + cm.display.lineDiv.style.cursor = ""; + } + if (val) { + data = cm.state.selectionPointer = { + value: typeof val == "string" ? val : "default", + mousemove: function(event) { mousemove(cm, event); }, + mouseout: function(event) { mouseout(cm, event); }, + windowScroll: function() { reset(cm); }, + rects: null, + mouseX: null, mouseY: null, + willUpdate: false + }; + CodeMirror.on(cm.getWrapperElement(), "mousemove", data.mousemove); + CodeMirror.on(cm.getWrapperElement(), "mouseout", data.mouseout); + CodeMirror.on(window, "scroll", data.windowScroll); + cm.on("cursorActivity", reset); + cm.on("scroll", reset); + } + }); + + function mousemove(cm, event) { + var data = cm.state.selectionPointer; + if (event.buttons == null ? event.which : event.buttons) { + data.mouseX = data.mouseY = null; + } else { + data.mouseX = event.clientX; + data.mouseY = event.clientY; + } + scheduleUpdate(cm); + } + + function mouseout(cm, event) { + if (!cm.getWrapperElement().contains(event.relatedTarget)) { + var data = cm.state.selectionPointer; + data.mouseX = data.mouseY = null; + scheduleUpdate(cm); + } + } + + function reset(cm) { + cm.state.selectionPointer.rects = null; + scheduleUpdate(cm); + } + + function scheduleUpdate(cm) { + if (!cm.state.selectionPointer.willUpdate) { + cm.state.selectionPointer.willUpdate = true; + setTimeout(function() { + update(cm); + cm.state.selectionPointer.willUpdate = false; + }, 50); + } + } + + function update(cm) { + var data = cm.state.selectionPointer; + if (!data) return; + if (data.rects == null && data.mouseX != null) { + data.rects = []; + if (cm.somethingSelected()) { + for (var sel = cm.display.selectionDiv.firstChild; sel; sel = sel.nextSibling) + data.rects.push(sel.getBoundingClientRect()); + } + } + var inside = false; + if (data.mouseX != null) for (var i = 0; i < data.rects.length; i++) { + var rect = data.rects[i]; + if (rect.left <= data.mouseX && rect.right >= data.mouseX && + rect.top <= data.mouseY && rect.bottom >= data.mouseY) + inside = true; + } + var cursor = inside ? data.value : ""; + if (cm.display.lineDiv.style.cursor != cursor) + cm.display.lineDiv.style.cursor = cursor; + } +}); diff --git a/public/vendor/plugins/codemirror/addon/tern/tern.css b/public/vendor/plugins/codemirror/addon/tern/tern.css new file mode 100644 index 0000000000..c4b8a2f77e --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/tern/tern.css @@ -0,0 +1,87 @@ +.CodeMirror-Tern-completion { + padding-left: 22px; + position: relative; + line-height: 1.5; +} +.CodeMirror-Tern-completion:before { + position: absolute; + left: 2px; + bottom: 2px; + border-radius: 50%; + font-size: 12px; + font-weight: bold; + height: 15px; + width: 15px; + line-height: 16px; + text-align: center; + color: white; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.CodeMirror-Tern-completion-unknown:before { + content: "?"; + background: #4bb; +} +.CodeMirror-Tern-completion-object:before { + content: "O"; + background: #77c; +} +.CodeMirror-Tern-completion-fn:before { + content: "F"; + background: #7c7; +} +.CodeMirror-Tern-completion-array:before { + content: "A"; + background: #c66; +} +.CodeMirror-Tern-completion-number:before { + content: "1"; + background: #999; +} +.CodeMirror-Tern-completion-string:before { + content: "S"; + background: #999; +} +.CodeMirror-Tern-completion-bool:before { + content: "B"; + background: #999; +} + +.CodeMirror-Tern-completion-guess { + color: #999; +} + +.CodeMirror-Tern-tooltip { + border: 1px solid silver; + border-radius: 3px; + color: #444; + padding: 2px 5px; + font-size: 90%; + font-family: monospace; + background-color: white; + white-space: pre-wrap; + + max-width: 40em; + position: absolute; + z-index: 10; + -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + box-shadow: 2px 3px 5px rgba(0,0,0,.2); + + transition: opacity 1s; + -moz-transition: opacity 1s; + -webkit-transition: opacity 1s; + -o-transition: opacity 1s; + -ms-transition: opacity 1s; +} + +.CodeMirror-Tern-hint-doc { + max-width: 25em; + margin-top: -3px; +} + +.CodeMirror-Tern-fname { color: black; } +.CodeMirror-Tern-farg { color: #70a; } +.CodeMirror-Tern-farg-current { text-decoration: underline; } +.CodeMirror-Tern-type { color: #07c; } +.CodeMirror-Tern-fhint-guess { opacity: .7; } diff --git a/public/vendor/plugins/codemirror/addon/tern/tern.js b/public/vendor/plugins/codemirror/addon/tern/tern.js new file mode 100644 index 0000000000..253309d678 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/tern/tern.js @@ -0,0 +1,718 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// Glue code between CodeMirror and Tern. +// +// Create a CodeMirror.TernServer to wrap an actual Tern server, +// register open documents (CodeMirror.Doc instances) with it, and +// call its methods to activate the assisting functions that Tern +// provides. +// +// Options supported (all optional): +// * defs: An array of JSON definition data structures. +// * plugins: An object mapping plugin names to configuration +// options. +// * getFile: A function(name, c) that can be used to access files in +// the project that haven't been loaded yet. Simply do c(null) to +// indicate that a file is not available. +// * fileFilter: A function(value, docName, doc) that will be applied +// to documents before passing them on to Tern. +// * switchToDoc: A function(name, doc) that should, when providing a +// multi-file view, switch the view or focus to the named file. +// * showError: A function(editor, message) that can be used to +// override the way errors are displayed. +// * completionTip: Customize the content in tooltips for completions. +// Is passed a single argument—the completion's data as returned by +// Tern—and may return a string, DOM node, or null to indicate that +// no tip should be shown. By default the docstring is shown. +// * typeTip: Like completionTip, but for the tooltips shown for type +// queries. +// * responseFilter: A function(doc, query, request, error, data) that +// will be applied to the Tern responses before treating them +// +// +// It is possible to run the Tern server in a web worker by specifying +// these additional options: +// * useWorker: Set to true to enable web worker mode. You'll probably +// want to feature detect the actual value you use here, for example +// !!window.Worker. +// * workerScript: The main script of the worker. Point this to +// wherever you are hosting worker.js from this directory. +// * workerDeps: An array of paths pointing (relative to workerScript) +// to the Acorn and Tern libraries and any Tern plugins you want to +// load. Or, if you minified those into a single script and included +// them in the workerScript, simply leave this undefined. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + // declare global: tern + + CodeMirror.TernServer = function(options) { + var self = this; + this.options = options || {}; + var plugins = this.options.plugins || (this.options.plugins = {}); + if (!plugins.doc_comment) plugins.doc_comment = true; + this.docs = Object.create(null); + if (this.options.useWorker) { + this.server = new WorkerServer(this); + } else { + this.server = new tern.Server({ + getFile: function(name, c) { return getFile(self, name, c); }, + async: true, + defs: this.options.defs || [], + plugins: plugins + }); + } + this.trackChange = function(doc, change) { trackChange(self, doc, change); }; + + this.cachedArgHints = null; + this.activeArgHints = null; + this.jumpStack = []; + + this.getHint = function(cm, c) { return hint(self, cm, c); }; + this.getHint.async = true; + }; + + CodeMirror.TernServer.prototype = { + addDoc: function(name, doc) { + var data = {doc: doc, name: name, changed: null}; + this.server.addFile(name, docValue(this, data)); + CodeMirror.on(doc, "change", this.trackChange); + return this.docs[name] = data; + }, + + delDoc: function(id) { + var found = resolveDoc(this, id); + if (!found) return; + CodeMirror.off(found.doc, "change", this.trackChange); + delete this.docs[found.name]; + this.server.delFile(found.name); + }, + + hideDoc: function(id) { + closeArgHints(this); + var found = resolveDoc(this, id); + if (found && found.changed) sendDoc(this, found); + }, + + complete: function(cm) { + cm.showHint({hint: this.getHint}); + }, + + showType: function(cm, pos, c) { showContextInfo(this, cm, pos, "type", c); }, + + showDocs: function(cm, pos, c) { showContextInfo(this, cm, pos, "documentation", c); }, + + updateArgHints: function(cm) { updateArgHints(this, cm); }, + + jumpToDef: function(cm) { jumpToDef(this, cm); }, + + jumpBack: function(cm) { jumpBack(this, cm); }, + + rename: function(cm) { rename(this, cm); }, + + selectName: function(cm) { selectName(this, cm); }, + + request: function (cm, query, c, pos) { + var self = this; + var doc = findDoc(this, cm.getDoc()); + var request = buildRequest(this, doc, query, pos); + var extraOptions = request.query && this.options.queryOptions && this.options.queryOptions[request.query.type] + if (extraOptions) for (var prop in extraOptions) request.query[prop] = extraOptions[prop]; + + this.server.request(request, function (error, data) { + if (!error && self.options.responseFilter) + data = self.options.responseFilter(doc, query, request, error, data); + c(error, data); + }); + }, + + destroy: function () { + closeArgHints(this) + if (this.worker) { + this.worker.terminate(); + this.worker = null; + } + } + }; + + var Pos = CodeMirror.Pos; + var cls = "CodeMirror-Tern-"; + var bigDoc = 250; + + function getFile(ts, name, c) { + var buf = ts.docs[name]; + if (buf) + c(docValue(ts, buf)); + else if (ts.options.getFile) + ts.options.getFile(name, c); + else + c(null); + } + + function findDoc(ts, doc, name) { + for (var n in ts.docs) { + var cur = ts.docs[n]; + if (cur.doc == doc) return cur; + } + if (!name) for (var i = 0;; ++i) { + n = "[doc" + (i || "") + "]"; + if (!ts.docs[n]) { name = n; break; } + } + return ts.addDoc(name, doc); + } + + function resolveDoc(ts, id) { + if (typeof id == "string") return ts.docs[id]; + if (id instanceof CodeMirror) id = id.getDoc(); + if (id instanceof CodeMirror.Doc) return findDoc(ts, id); + } + + function trackChange(ts, doc, change) { + var data = findDoc(ts, doc); + + var argHints = ts.cachedArgHints; + if (argHints && argHints.doc == doc && cmpPos(argHints.start, change.to) >= 0) + ts.cachedArgHints = null; + + var changed = data.changed; + if (changed == null) + data.changed = changed = {from: change.from.line, to: change.from.line}; + var end = change.from.line + (change.text.length - 1); + if (change.from.line < changed.to) changed.to = changed.to - (change.to.line - end); + if (end >= changed.to) changed.to = end + 1; + if (changed.from > change.from.line) changed.from = change.from.line; + + if (doc.lineCount() > bigDoc && change.to - changed.from > 100) setTimeout(function() { + if (data.changed && data.changed.to - data.changed.from > 100) sendDoc(ts, data); + }, 200); + } + + function sendDoc(ts, doc) { + ts.server.request({files: [{type: "full", name: doc.name, text: docValue(ts, doc)}]}, function(error) { + if (error) window.console.error(error); + else doc.changed = null; + }); + } + + // Completion + + function hint(ts, cm, c) { + ts.request(cm, {type: "completions", types: true, docs: true, urls: true}, function(error, data) { + if (error) return showError(ts, cm, error); + var completions = [], after = ""; + var from = data.start, to = data.end; + if (cm.getRange(Pos(from.line, from.ch - 2), from) == "[\"" && + cm.getRange(to, Pos(to.line, to.ch + 2)) != "\"]") + after = "\"]"; + + for (var i = 0; i < data.completions.length; ++i) { + var completion = data.completions[i], className = typeToIcon(completion.type); + if (data.guess) className += " " + cls + "guess"; + completions.push({text: completion.name + after, + displayText: completion.displayName || completion.name, + className: className, + data: completion}); + } + + var obj = {from: from, to: to, list: completions}; + var tooltip = null; + CodeMirror.on(obj, "close", function() { remove(tooltip); }); + CodeMirror.on(obj, "update", function() { remove(tooltip); }); + CodeMirror.on(obj, "select", function(cur, node) { + remove(tooltip); + var content = ts.options.completionTip ? ts.options.completionTip(cur.data) : cur.data.doc; + if (content) { + tooltip = makeTooltip(node.parentNode.getBoundingClientRect().right + window.pageXOffset, + node.getBoundingClientRect().top + window.pageYOffset, content); + tooltip.className += " " + cls + "hint-doc"; + } + }); + c(obj); + }); + } + + function typeToIcon(type) { + var suffix; + if (type == "?") suffix = "unknown"; + else if (type == "number" || type == "string" || type == "bool") suffix = type; + else if (/^fn\(/.test(type)) suffix = "fn"; + else if (/^\[/.test(type)) suffix = "array"; + else suffix = "object"; + return cls + "completion " + cls + "completion-" + suffix; + } + + // Type queries + + function showContextInfo(ts, cm, pos, queryName, c) { + ts.request(cm, queryName, function(error, data) { + if (error) return showError(ts, cm, error); + if (ts.options.typeTip) { + var tip = ts.options.typeTip(data); + } else { + var tip = elt("span", null, elt("strong", null, data.type || "not found")); + if (data.doc) + tip.appendChild(document.createTextNode(" — " + data.doc)); + if (data.url) { + tip.appendChild(document.createTextNode(" ")); + var child = tip.appendChild(elt("a", null, "[docs]")); + child.href = data.url; + child.target = "_blank"; + } + } + tempTooltip(cm, tip, ts); + if (c) c(); + }, pos); + } + + // Maintaining argument hints + + function updateArgHints(ts, cm) { + closeArgHints(ts); + + if (cm.somethingSelected()) return; + var state = cm.getTokenAt(cm.getCursor()).state; + var inner = CodeMirror.innerMode(cm.getMode(), state); + if (inner.mode.name != "javascript") return; + var lex = inner.state.lexical; + if (lex.info != "call") return; + + var ch, argPos = lex.pos || 0, tabSize = cm.getOption("tabSize"); + for (var line = cm.getCursor().line, e = Math.max(0, line - 9), found = false; line >= e; --line) { + var str = cm.getLine(line), extra = 0; + for (var pos = 0;;) { + var tab = str.indexOf("\t", pos); + if (tab == -1) break; + extra += tabSize - (tab + extra) % tabSize - 1; + pos = tab + 1; + } + ch = lex.column - extra; + if (str.charAt(ch) == "(") {found = true; break;} + } + if (!found) return; + + var start = Pos(line, ch); + var cache = ts.cachedArgHints; + if (cache && cache.doc == cm.getDoc() && cmpPos(start, cache.start) == 0) + return showArgHints(ts, cm, argPos); + + ts.request(cm, {type: "type", preferFunction: true, end: start}, function(error, data) { + if (error || !data.type || !(/^fn\(/).test(data.type)) return; + ts.cachedArgHints = { + start: start, + type: parseFnType(data.type), + name: data.exprName || data.name || "fn", + guess: data.guess, + doc: cm.getDoc() + }; + showArgHints(ts, cm, argPos); + }); + } + + function showArgHints(ts, cm, pos) { + closeArgHints(ts); + + var cache = ts.cachedArgHints, tp = cache.type; + var tip = elt("span", cache.guess ? cls + "fhint-guess" : null, + elt("span", cls + "fname", cache.name), "("); + for (var i = 0; i < tp.args.length; ++i) { + if (i) tip.appendChild(document.createTextNode(", ")); + var arg = tp.args[i]; + tip.appendChild(elt("span", cls + "farg" + (i == pos ? " " + cls + "farg-current" : ""), arg.name || "?")); + if (arg.type != "?") { + tip.appendChild(document.createTextNode(":\u00a0")); + tip.appendChild(elt("span", cls + "type", arg.type)); + } + } + tip.appendChild(document.createTextNode(tp.rettype ? ") ->\u00a0" : ")")); + if (tp.rettype) tip.appendChild(elt("span", cls + "type", tp.rettype)); + var place = cm.cursorCoords(null, "page"); + var tooltip = ts.activeArgHints = makeTooltip(place.right + 1, place.bottom, tip) + setTimeout(function() { + tooltip.clear = onEditorActivity(cm, function() { + if (ts.activeArgHints == tooltip) closeArgHints(ts) }) + }, 20) + } + + function parseFnType(text) { + var args = [], pos = 3; + + function skipMatching(upto) { + var depth = 0, start = pos; + for (;;) { + var next = text.charAt(pos); + if (upto.test(next) && !depth) return text.slice(start, pos); + if (/[{\[\(]/.test(next)) ++depth; + else if (/[}\]\)]/.test(next)) --depth; + ++pos; + } + } + + // Parse arguments + if (text.charAt(pos) != ")") for (;;) { + var name = text.slice(pos).match(/^([^, \(\[\{]+): /); + if (name) { + pos += name[0].length; + name = name[1]; + } + args.push({name: name, type: skipMatching(/[\),]/)}); + if (text.charAt(pos) == ")") break; + pos += 2; + } + + var rettype = text.slice(pos).match(/^\) -> (.*)$/); + + return {args: args, rettype: rettype && rettype[1]}; + } + + // Moving to the definition of something + + function jumpToDef(ts, cm) { + function inner(varName) { + var req = {type: "definition", variable: varName || null}; + var doc = findDoc(ts, cm.getDoc()); + ts.server.request(buildRequest(ts, doc, req), function(error, data) { + if (error) return showError(ts, cm, error); + if (!data.file && data.url) { window.open(data.url); return; } + + if (data.file) { + var localDoc = ts.docs[data.file], found; + if (localDoc && (found = findContext(localDoc.doc, data))) { + ts.jumpStack.push({file: doc.name, + start: cm.getCursor("from"), + end: cm.getCursor("to")}); + moveTo(ts, doc, localDoc, found.start, found.end); + return; + } + } + showError(ts, cm, "Could not find a definition."); + }); + } + + if (!atInterestingExpression(cm)) + dialog(cm, "Jump to variable", function(name) { if (name) inner(name); }); + else + inner(); + } + + function jumpBack(ts, cm) { + var pos = ts.jumpStack.pop(), doc = pos && ts.docs[pos.file]; + if (!doc) return; + moveTo(ts, findDoc(ts, cm.getDoc()), doc, pos.start, pos.end); + } + + function moveTo(ts, curDoc, doc, start, end) { + doc.doc.setSelection(start, end); + if (curDoc != doc && ts.options.switchToDoc) { + closeArgHints(ts); + ts.options.switchToDoc(doc.name, doc.doc); + } + } + + // The {line,ch} representation of positions makes this rather awkward. + function findContext(doc, data) { + var before = data.context.slice(0, data.contextOffset).split("\n"); + var startLine = data.start.line - (before.length - 1); + var start = Pos(startLine, (before.length == 1 ? data.start.ch : doc.getLine(startLine).length) - before[0].length); + + var text = doc.getLine(startLine).slice(start.ch); + for (var cur = startLine + 1; cur < doc.lineCount() && text.length < data.context.length; ++cur) + text += "\n" + doc.getLine(cur); + if (text.slice(0, data.context.length) == data.context) return data; + + var cursor = doc.getSearchCursor(data.context, 0, false); + var nearest, nearestDist = Infinity; + while (cursor.findNext()) { + var from = cursor.from(), dist = Math.abs(from.line - start.line) * 10000; + if (!dist) dist = Math.abs(from.ch - start.ch); + if (dist < nearestDist) { nearest = from; nearestDist = dist; } + } + if (!nearest) return null; + + if (before.length == 1) + nearest.ch += before[0].length; + else + nearest = Pos(nearest.line + (before.length - 1), before[before.length - 1].length); + if (data.start.line == data.end.line) + var end = Pos(nearest.line, nearest.ch + (data.end.ch - data.start.ch)); + else + var end = Pos(nearest.line + (data.end.line - data.start.line), data.end.ch); + return {start: nearest, end: end}; + } + + function atInterestingExpression(cm) { + var pos = cm.getCursor("end"), tok = cm.getTokenAt(pos); + if (tok.start < pos.ch && tok.type == "comment") return false; + return /[\w)\]]/.test(cm.getLine(pos.line).slice(Math.max(pos.ch - 1, 0), pos.ch + 1)); + } + + // Variable renaming + + function rename(ts, cm) { + var token = cm.getTokenAt(cm.getCursor()); + if (!/\w/.test(token.string)) return showError(ts, cm, "Not at a variable"); + dialog(cm, "New name for " + token.string, function(newName) { + ts.request(cm, {type: "rename", newName: newName, fullDocs: true}, function(error, data) { + if (error) return showError(ts, cm, error); + applyChanges(ts, data.changes); + }); + }); + } + + function selectName(ts, cm) { + var name = findDoc(ts, cm.doc).name; + ts.request(cm, {type: "refs"}, function(error, data) { + if (error) return showError(ts, cm, error); + var ranges = [], cur = 0; + var curPos = cm.getCursor(); + for (var i = 0; i < data.refs.length; i++) { + var ref = data.refs[i]; + if (ref.file == name) { + ranges.push({anchor: ref.start, head: ref.end}); + if (cmpPos(curPos, ref.start) >= 0 && cmpPos(curPos, ref.end) <= 0) + cur = ranges.length - 1; + } + } + cm.setSelections(ranges, cur); + }); + } + + var nextChangeOrig = 0; + function applyChanges(ts, changes) { + var perFile = Object.create(null); + for (var i = 0; i < changes.length; ++i) { + var ch = changes[i]; + (perFile[ch.file] || (perFile[ch.file] = [])).push(ch); + } + for (var file in perFile) { + var known = ts.docs[file], chs = perFile[file];; + if (!known) continue; + chs.sort(function(a, b) { return cmpPos(b.start, a.start); }); + var origin = "*rename" + (++nextChangeOrig); + for (var i = 0; i < chs.length; ++i) { + var ch = chs[i]; + known.doc.replaceRange(ch.text, ch.start, ch.end, origin); + } + } + } + + // Generic request-building helper + + function buildRequest(ts, doc, query, pos) { + var files = [], offsetLines = 0, allowFragments = !query.fullDocs; + if (!allowFragments) delete query.fullDocs; + if (typeof query == "string") query = {type: query}; + query.lineCharPositions = true; + if (query.end == null) { + query.end = pos || doc.doc.getCursor("end"); + if (doc.doc.somethingSelected()) + query.start = doc.doc.getCursor("start"); + } + var startPos = query.start || query.end; + + if (doc.changed) { + if (doc.doc.lineCount() > bigDoc && allowFragments !== false && + doc.changed.to - doc.changed.from < 100 && + doc.changed.from <= startPos.line && doc.changed.to > query.end.line) { + files.push(getFragmentAround(doc, startPos, query.end)); + query.file = "#0"; + var offsetLines = files[0].offsetLines; + if (query.start != null) query.start = Pos(query.start.line - -offsetLines, query.start.ch); + query.end = Pos(query.end.line - offsetLines, query.end.ch); + } else { + files.push({type: "full", + name: doc.name, + text: docValue(ts, doc)}); + query.file = doc.name; + doc.changed = null; + } + } else { + query.file = doc.name; + } + for (var name in ts.docs) { + var cur = ts.docs[name]; + if (cur.changed && cur != doc) { + files.push({type: "full", name: cur.name, text: docValue(ts, cur)}); + cur.changed = null; + } + } + + return {query: query, files: files}; + } + + function getFragmentAround(data, start, end) { + var doc = data.doc; + var minIndent = null, minLine = null, endLine, tabSize = 4; + for (var p = start.line - 1, min = Math.max(0, p - 50); p >= min; --p) { + var line = doc.getLine(p), fn = line.search(/\bfunction\b/); + if (fn < 0) continue; + var indent = CodeMirror.countColumn(line, null, tabSize); + if (minIndent != null && minIndent <= indent) continue; + minIndent = indent; + minLine = p; + } + if (minLine == null) minLine = min; + var max = Math.min(doc.lastLine(), end.line + 20); + if (minIndent == null || minIndent == CodeMirror.countColumn(doc.getLine(start.line), null, tabSize)) + endLine = max; + else for (endLine = end.line + 1; endLine < max; ++endLine) { + var indent = CodeMirror.countColumn(doc.getLine(endLine), null, tabSize); + if (indent <= minIndent) break; + } + var from = Pos(minLine, 0); + + return {type: "part", + name: data.name, + offsetLines: from.line, + text: doc.getRange(from, Pos(endLine, end.line == endLine ? null : 0))}; + } + + // Generic utilities + + var cmpPos = CodeMirror.cmpPos; + + function elt(tagname, cls /*, ... elts*/) { + var e = document.createElement(tagname); + if (cls) e.className = cls; + for (var i = 2; i < arguments.length; ++i) { + var elt = arguments[i]; + if (typeof elt == "string") elt = document.createTextNode(elt); + e.appendChild(elt); + } + return e; + } + + function dialog(cm, text, f) { + if (cm.openDialog) + cm.openDialog(text + ": ", f); + else + f(prompt(text, "")); + } + + // Tooltips + + function tempTooltip(cm, content, ts) { + if (cm.state.ternTooltip) remove(cm.state.ternTooltip); + var where = cm.cursorCoords(); + var tip = cm.state.ternTooltip = makeTooltip(where.right + 1, where.bottom, content); + function maybeClear() { + old = true; + if (!mouseOnTip) clear(); + } + function clear() { + cm.state.ternTooltip = null; + if (tip.parentNode) fadeOut(tip) + clearActivity() + } + var mouseOnTip = false, old = false; + CodeMirror.on(tip, "mousemove", function() { mouseOnTip = true; }); + CodeMirror.on(tip, "mouseout", function(e) { + var related = e.relatedTarget || e.toElement + if (!related || !CodeMirror.contains(tip, related)) { + if (old) clear(); + else mouseOnTip = false; + } + }); + setTimeout(maybeClear, ts.options.hintDelay ? ts.options.hintDelay : 1700); + var clearActivity = onEditorActivity(cm, clear) + } + + function onEditorActivity(cm, f) { + cm.on("cursorActivity", f) + cm.on("blur", f) + cm.on("scroll", f) + cm.on("setDoc", f) + return function() { + cm.off("cursorActivity", f) + cm.off("blur", f) + cm.off("scroll", f) + cm.off("setDoc", f) + } + } + + function makeTooltip(x, y, content) { + var node = elt("div", cls + "tooltip", content); + node.style.left = x + "px"; + node.style.top = y + "px"; + document.body.appendChild(node); + return node; + } + + function remove(node) { + var p = node && node.parentNode; + if (p) p.removeChild(node); + } + + function fadeOut(tooltip) { + tooltip.style.opacity = "0"; + setTimeout(function() { remove(tooltip); }, 1100); + } + + function showError(ts, cm, msg) { + if (ts.options.showError) + ts.options.showError(cm, msg); + else + tempTooltip(cm, String(msg), ts); + } + + function closeArgHints(ts) { + if (ts.activeArgHints) { + if (ts.activeArgHints.clear) ts.activeArgHints.clear() + remove(ts.activeArgHints) + ts.activeArgHints = null + } + } + + function docValue(ts, doc) { + var val = doc.doc.getValue(); + if (ts.options.fileFilter) val = ts.options.fileFilter(val, doc.name, doc.doc); + return val; + } + + // Worker wrapper + + function WorkerServer(ts) { + var worker = ts.worker = new Worker(ts.options.workerScript); + worker.postMessage({type: "init", + defs: ts.options.defs, + plugins: ts.options.plugins, + scripts: ts.options.workerDeps}); + var msgId = 0, pending = {}; + + function send(data, c) { + if (c) { + data.id = ++msgId; + pending[msgId] = c; + } + worker.postMessage(data); + } + worker.onmessage = function(e) { + var data = e.data; + if (data.type == "getFile") { + getFile(ts, data.name, function(err, text) { + send({type: "getFile", err: String(err), text: text, id: data.id}); + }); + } else if (data.type == "debug") { + window.console.log(data.message); + } else if (data.id && pending[data.id]) { + pending[data.id](data.err, data.body); + delete pending[data.id]; + } + }; + worker.onerror = function(e) { + for (var id in pending) pending[id](e); + pending = {}; + }; + + this.addFile = function(name, text) { send({type: "add", name: name, text: text}); }; + this.delFile = function(name) { send({type: "del", name: name}); }; + this.request = function(body, c) { send({type: "req", body: body}, c); }; + } +}); diff --git a/public/vendor/plugins/codemirror/addon/tern/worker.js b/public/vendor/plugins/codemirror/addon/tern/worker.js new file mode 100644 index 0000000000..e134ad47d6 --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/tern/worker.js @@ -0,0 +1,44 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// declare global: tern, server + +var server; + +this.onmessage = function(e) { + var data = e.data; + switch (data.type) { + case "init": return startServer(data.defs, data.plugins, data.scripts); + case "add": return server.addFile(data.name, data.text); + case "del": return server.delFile(data.name); + case "req": return server.request(data.body, function(err, reqData) { + postMessage({id: data.id, body: reqData, err: err && String(err)}); + }); + case "getFile": + var c = pending[data.id]; + delete pending[data.id]; + return c(data.err, data.text); + default: throw new Error("Unknown message type: " + data.type); + } +}; + +var nextId = 0, pending = {}; +function getFile(file, c) { + postMessage({type: "getFile", name: file, id: ++nextId}); + pending[nextId] = c; +} + +function startServer(defs, plugins, scripts) { + if (scripts) importScripts.apply(null, scripts); + + server = new tern.Server({ + getFile: getFile, + async: true, + defs: defs, + plugins: plugins + }); +} + +this.console = { + log: function(v) { postMessage({type: "debug", message: v}); } +}; diff --git a/public/vendor/plugins/codemirror/addon/wrap/hardwrap.js b/public/vendor/plugins/codemirror/addon/wrap/hardwrap.js new file mode 100644 index 0000000000..29cc15f01f --- /dev/null +++ b/public/vendor/plugins/codemirror/addon/wrap/hardwrap.js @@ -0,0 +1,145 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + "use strict"; + + var Pos = CodeMirror.Pos; + + function findParagraph(cm, pos, options) { + var startRE = options.paragraphStart || cm.getHelper(pos, "paragraphStart"); + for (var start = pos.line, first = cm.firstLine(); start > first; --start) { + var line = cm.getLine(start); + if (startRE && startRE.test(line)) break; + if (!/\S/.test(line)) { ++start; break; } + } + var endRE = options.paragraphEnd || cm.getHelper(pos, "paragraphEnd"); + for (var end = pos.line + 1, last = cm.lastLine(); end <= last; ++end) { + var line = cm.getLine(end); + if (endRE && endRE.test(line)) { ++end; break; } + if (!/\S/.test(line)) break; + } + return {from: start, to: end}; + } + + function findBreakPoint(text, column, wrapOn, killTrailingSpace) { + var at = column + while (at < text.length && text.charAt(at) == " ") at++ + for (; at > 0; --at) + if (wrapOn.test(text.slice(at - 1, at + 1))) break; + for (var first = true;; first = false) { + var endOfText = at; + if (killTrailingSpace) + while (text.charAt(endOfText - 1) == " ") --endOfText; + if (endOfText == 0 && first) at = column; + else return {from: endOfText, to: at}; + } + } + + function wrapRange(cm, from, to, options) { + from = cm.clipPos(from); to = cm.clipPos(to); + var column = options.column || 80; + var wrapOn = options.wrapOn || /\s\S|-[^\.\d]/; + var killTrailing = options.killTrailingSpace !== false; + var changes = [], curLine = "", curNo = from.line; + var lines = cm.getRange(from, to, false); + if (!lines.length) return null; + var leadingSpace = lines[0].match(/^[ \t]*/)[0]; + if (leadingSpace.length >= column) column = leadingSpace.length + 1 + + for (var i = 0; i < lines.length; ++i) { + var text = lines[i], oldLen = curLine.length, spaceInserted = 0; + if (curLine && text && !wrapOn.test(curLine.charAt(curLine.length - 1) + text.charAt(0))) { + curLine += " "; + spaceInserted = 1; + } + var spaceTrimmed = ""; + if (i) { + spaceTrimmed = text.match(/^\s*/)[0]; + text = text.slice(spaceTrimmed.length); + } + curLine += text; + if (i) { + var firstBreak = curLine.length > column && leadingSpace == spaceTrimmed && + findBreakPoint(curLine, column, wrapOn, killTrailing); + // If this isn't broken, or is broken at a different point, remove old break + if (!firstBreak || firstBreak.from != oldLen || firstBreak.to != oldLen + spaceInserted) { + changes.push({text: [spaceInserted ? " " : ""], + from: Pos(curNo, oldLen), + to: Pos(curNo + 1, spaceTrimmed.length)}); + } else { + curLine = leadingSpace + text; + ++curNo; + } + } + while (curLine.length > column) { + var bp = findBreakPoint(curLine, column, wrapOn, killTrailing); + changes.push({text: ["", leadingSpace], + from: Pos(curNo, bp.from), + to: Pos(curNo, bp.to)}); + curLine = leadingSpace + curLine.slice(bp.to); + ++curNo; + } + } + if (changes.length) cm.operation(function() { + for (var i = 0; i < changes.length; ++i) { + var change = changes[i]; + if (change.text || CodeMirror.cmpPos(change.from, change.to)) + cm.replaceRange(change.text, change.from, change.to); + } + }); + return changes.length ? {from: changes[0].from, to: CodeMirror.changeEnd(changes[changes.length - 1])} : null; + } + + CodeMirror.defineExtension("wrapParagraph", function(pos, options) { + options = options || {}; + if (!pos) pos = this.getCursor(); + var para = findParagraph(this, pos, options); + return wrapRange(this, Pos(para.from, 0), Pos(para.to - 1), options); + }); + + CodeMirror.commands.wrapLines = function(cm) { + cm.operation(function() { + var ranges = cm.listSelections(), at = cm.lastLine() + 1; + for (var i = ranges.length - 1; i >= 0; i--) { + var range = ranges[i], span; + if (range.empty()) { + var para = findParagraph(cm, range.head, {}); + span = {from: Pos(para.from, 0), to: Pos(para.to - 1)}; + } else { + span = {from: range.from(), to: range.to()}; + } + if (span.to.line >= at) continue; + at = span.from.line; + wrapRange(cm, span.from, span.to, {}); + } + }); + }; + + CodeMirror.defineExtension("wrapRange", function(from, to, options) { + return wrapRange(this, from, to, options || {}); + }); + + CodeMirror.defineExtension("wrapParagraphsInRange", function(from, to, options) { + options = options || {}; + var cm = this, paras = []; + for (var line = from.line; line <= to.line;) { + var para = findParagraph(cm, Pos(line, 0), options); + paras.push(para); + line = para.to; + } + var madeChange = false; + if (paras.length) cm.operation(function() { + for (var i = paras.length - 1; i >= 0; --i) + madeChange = madeChange || wrapRange(cm, Pos(paras[i].from, 0), Pos(paras[i].to - 1), options); + }); + return madeChange; + }); +}); diff --git a/public/vendor/plugins/codemirror/mode/apl/apl.js b/public/vendor/plugins/codemirror/mode/apl/apl.js index caafe4e913..b1955f6c94 100644 --- a/public/vendor/plugins/codemirror/mode/apl/apl.js +++ b/public/vendor/plugins/codemirror/mode/apl/apl.js @@ -1,5 +1,5 @@ // CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE +// Distributed under an MIT license: https://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS diff --git a/public/vendor/plugins/codemirror/mode/apl/index.html b/public/vendor/plugins/codemirror/mode/apl/index.html index 53dda6b586..56ab02ffba 100644 --- a/public/vendor/plugins/codemirror/mode/apl/index.html +++ b/public/vendor/plugins/codemirror/mode/apl/index.html @@ -12,7 +12,7 @@ .CodeMirror { border: 2px inset #dee; }