From 8a828500e689b7958e050ebd2dc46d380c3d6ae6 Mon Sep 17 00:00:00 2001 From: 8ctopus <13252042+8ctopus@users.noreply.github.com> Date: Sat, 5 Oct 2019 19:16:30 +0500 Subject: [PATCH 001/154] Doc config file should not be readable by others as it contains sensitive info (#8385) --- .../doc/installation/from-binary.en-us.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/content/doc/installation/from-binary.en-us.md b/docs/content/doc/installation/from-binary.en-us.md index c93973f222..10f0ff15b6 100644 --- a/docs/content/doc/installation/from-binary.en-us.md +++ b/docs/content/doc/installation/from-binary.en-us.md @@ -44,7 +44,7 @@ location. When launched manually, Gitea can be killed using `Ctrl+C`. ## Recommended server configuration -**NOTE:** Many of the following directories can be configured using [Environment Variables]({{< relref "doc/advanced/specific-variables.en-us.md" >}}) 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. From 93e2ce699babb60818c0d5e75eda39f0e9263216 Mon Sep 17 00:00:00 2001 From: 8ctopus <13252042+8ctopus@users.noreply.github.com> Date: Sun, 6 Oct 2019 09:38:10 +0500 Subject: [PATCH 002/154] Doc added instructions for Git LFS support (#8391) --- docs/content/doc/usage/git-lfs-support.md | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/content/doc/usage/git-lfs-support.md 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 From bc5a479fefa77ee54a9fddecdbbb7e7991f22da1 Mon Sep 17 00:00:00 2001 From: Thomas McWork Date: Sun, 6 Oct 2019 20:32:23 +0200 Subject: [PATCH 003/154] Add unix socket help (#8377) When using unix socket as listener (`HTTP_ADDR = /run/gitea/gitea.socket`) then it's required to have the folder `/run/gitea` with appropriate owner/group. Manual creation leads to vanishing after reboot. This directive enables Systemd to handle this. --- contrib/systemd/gitea.service | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service index d88df4a037..b7e6629ebf 100644 --- a/contrib/systemd/gitea.service +++ b/contrib/systemd/gitea.service @@ -20,6 +20,9 @@ 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 From 51fade4c44c3517923cd07783ab05a55aaa84dcd Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 7 Oct 2019 05:26:19 +0800 Subject: [PATCH 004/154] Fix milestone num_issues (#8221) * fix milestone num_issues * update missing completeness * only update milestone closed number when closed issue is assigned a new milestone or clear milestone * fix tests * fix update milestone num * fix completeness calculate * make completeness calucation more clear --- models/issue.go | 4 +- models/issue_milestone.go | 79 ++++++++++++++++++---------------- models/issue_milestone_test.go | 6 +-- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/models/issue.go b/models/issue.go index 9590bc04ff..e4cc1291c2 100644 --- a/models/issue.go +++ b/models/issue.go @@ -766,7 +766,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 } @@ -1119,7 +1119,7 @@ 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 } } diff --git a/models/issue_milestone.go b/models/issue_milestone.go index f8f414e716..29e13689bf 100644 --- a/models/issue_milestone.go +++ b/models/issue_milestone.go @@ -311,71 +311,74 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { return sess.Commit() } -func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error { - if issue.MilestoneID == 0 { - return nil +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 + _, 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 +} + +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) + _, 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 } 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{}) } From 08896cd9f657c3697af0ed2415a8461080a0eb0a Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 7 Oct 2019 06:59:17 +0200 Subject: [PATCH 005/154] add file line count info on UI (#8396) Also reworked the header to remove the filename (which is redundant with the file path above) and made the header a flexbox with a monospace font. Fixes: https://github.com/go-gitea/gitea/issues/8170 --- options/locale/locale_en-US.ini | 1 + public/css/index.css | 5 +- public/less/_repository.less | 20 +++++++- routers/repo/view.go | 2 + templates/repo/view_file.tmpl | 84 +++++++++++++++++---------------- 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index c8be059b72..d9d27af26f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -681,6 +681,7 @@ stored_lfs = Stored with Git LFS commit_graph = Commit Graph blame = Blame normal_view = Normal View +lines = lines editor.new_file = New File editor.upload_file = Upload File diff --git a/public/css/index.css b/public/css/index.css index 496194decc..0efd787122 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -494,7 +494,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} @@ -878,6 +878,9 @@ 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} +.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/less/_repository.less b/public/less/_repository.less index ade3477ccc..0527759ed4 100644 --- a/public/less/_repository.less +++ b/public/less/_repository.less @@ -371,9 +371,7 @@ } .file-actions { - margin-top: 0; margin-bottom: -5px; - padding-left: 20px; .btn-octicon { display: inline-block; @@ -2385,3 +2383,21 @@ tbody.commit-list { padding-top: 8px; padding-bottom: 8px; } + +.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/routers/repo/view.go b/routers/repo/view.go index 00790a4ef3..1967b511ca 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -304,6 +304,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st var output bytes.Buffer lines := strings.Split(fileContent, "\n") + ctx.Data["NumLines"] = len(lines) + //Remove blank line at the end of file if len(lines) > 0 && lines[len(lines)-1] == "" { lines = lines[:len(lines)-1] diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 50e4c33946..71607bd1f8 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -1,48 +1,52 @@
-

-
-
- {{if .ReadmeExist}} - - {{if .ReadmeInList}} - {{.FileName}} - {{else}} - {{.FileName}} {{FileSize .FileSize}}{{if .IsLFSFile}} ({{.i18n.Tr "repo.stored_lfs"}}){{end}} - {{end}} - {{else}} - - {{.FileName}} {{FileSize .FileSize}}{{if .IsLFSFile}} ({{.i18n.Tr "repo.stored_lfs"}}){{end}} - {{end}} -
-
- {{if not .ReadmeInList}} -
-
- {{.i18n.Tr "repo.file_raw"}} - {{if not .IsViewCommit}} - {{.i18n.Tr "repo.file_permalink"}} - {{end}} - {{if .IsTextFile}} - {{.i18n.Tr "repo.blame"}} - {{end}} - {{.i18n.Tr "repo.file_history"}} +

+
+ {{if .ReadmeInList}} + + {{.FileName}} + {{else}} +
+ {{if .NumLines}} +
+ {{.NumLines}} {{.i18n.Tr "repo.lines"}}
- {{if .Repository.CanEnableEditor}} - {{if .CanEditFile}} - - {{else}} - - {{end}} - {{if .CanDeleteFile}} - - {{else}} - - {{end}} - {{end}} -
+ {{end}} + {{if .FileSize}} +
+ {{FileSize .FileSize}}{{if .IsLFSFile}} ({{.i18n.Tr "repo.stored_lfs"}}){{end}} +
+ {{end}} +
+ {{end}} +

+ {{if not .ReadmeInList}} +
+
+
+ {{.i18n.Tr "repo.file_raw"}} + {{if not .IsViewCommit}} + {{.i18n.Tr "repo.file_permalink"}} + {{end}} + {{if .IsTextFile}} + {{.i18n.Tr "repo.blame"}} + {{end}} + {{.i18n.Tr "repo.file_history"}} +
+ {{if .Repository.CanEnableEditor}} + {{if .CanEditFile}} + + {{else}} + + {{end}} + {{if .CanDeleteFile}} + + {{else}} + + {{end}} {{end}}
+ {{end}}

From 356e1a70ea4fb8b30ac2014284511773ce59bcf5 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Mon, 7 Oct 2019 02:49:14 -0300 Subject: [PATCH 006/154] Reduce test sensibility (#8393) --- modules/charset/charset_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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} From 249dbbe0bcc57e3034321fdf7b933c62f34cfa83 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 7 Oct 2019 19:22:35 +0200 Subject: [PATCH 007/154] Update golangci to v1.19.1 (#8414) Signed-off-by: kolaente --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b881bc9553..e8220ef36c 100644 --- a/Makefile +++ b/Makefile @@ -515,6 +515,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.19.1; \ fi golangci-lint run --deadline=3m From af6cc5b9d86ea8e4afa7e906f6bd2b4af3cfe545 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 7 Oct 2019 17:24:26 +0000 Subject: [PATCH 008/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 11 +++++++++++ options/locale/locale_pt-BR.ini | 2 ++ 2 files changed, 13 insertions(+) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 3add4be1a9..fcec489af8 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -318,6 +318,7 @@ 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. @@ -679,6 +680,7 @@ stored_lfs=Gespeichert mit Git LFS commit_graph=Commit graph blame=Blame normal_view=Normale Ansicht +lines=Zeilen editor.new_file=Neue Datei editor.upload_file=Datei hochladen @@ -704,6 +706,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 @@ -1131,6 +1134,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 +1216,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). @@ -1469,6 +1478,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) diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 66c2d9fd4c..76890ddc79 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -680,6 +680,7 @@ stored_lfs=Armazenado com Git LFS commit_graph=Gráfico de commits blame=Anotar normal_view=Visão normal +lines=linhas editor.new_file=Novo arquivo editor.upload_file=Enviar arquivo @@ -705,6 +706,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 From 1b96c4a471e016c5c61cec5caa54ebc00401a961 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Mon, 7 Oct 2019 16:51:54 -0300 Subject: [PATCH 009/154] Fix backers badge (#8399) --- README.md | 2 +- README_ZH.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 92ed78a497..5d565e4a3a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![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) ## Purpose diff --git a/README_ZH.md b/README_ZH.md index e143f23b41..b546a15a3e 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -9,7 +9,7 @@ [![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) ## 目标 From 662a40ea29261167f43bdd3f695a6b22e2958de0 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 8 Oct 2019 05:44:58 +0800 Subject: [PATCH 010/154] Update milestone issues numbers when save milestone and other code improvements (#8411) * update milestone issues numbers when save milestone and other code improvements * fix tests * extract duplicate codes as a new function --- models/issue_milestone.go | 67 ++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/models/issue_milestone.go b/models/issue_milestone.go index 29e13689bf..1587e5e341 100644 --- a/models/issue_milestone.go +++ b/models/issue_milestone.go @@ -10,6 +10,7 @@ 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" ) @@ -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,27 +306,27 @@ 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 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, @@ -319,11 +335,7 @@ func updateMilestoneTotalNum(e Engine, milestoneID int64) (err error) { return } - _, 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 + return updateMilestoneCompleteness(e, milestoneID) } func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) { @@ -335,10 +347,7 @@ func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) { return } - _, 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 + return updateMilestoneCompleteness(e, milestoneID) } func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error { From 28d5347cf3da0a0e3c192f39b553c0ae6df53a09 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Tue, 8 Oct 2019 02:38:41 +0300 Subject: [PATCH 011/154] Singular form for files that has only one line (#8416) --- options/locale/locale_en-US.ini | 1 + templates/repo/view_file.tmpl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index d9d27af26f..ca09b6120d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -681,6 +681,7 @@ 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 diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 71607bd1f8..e3d346db3e 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -8,7 +8,7 @@
{{if .NumLines}}
- {{.NumLines}} {{.i18n.Tr "repo.lines"}} + {{.NumLines}} {{.i18n.Tr (TrN .i18n.Lang .NumLines "repo.line" "repo.lines") }}
{{end}} {{if .FileSize}} From 1a269f7ef81b1a7865c4679b91a4037059ad4938 Mon Sep 17 00:00:00 2001 From: 6543 <24977596+6543@users.noreply.github.com> Date: Tue, 8 Oct 2019 04:03:44 +0200 Subject: [PATCH 012/154] add 6543 to maintainers (#8417) --- MAINTAINERS | 1 + 1 file changed, 1 insertion(+) 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) From 78438d310be42f9c5e0e2937ee54e6050cc8f381 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 8 Oct 2019 10:57:41 +0800 Subject: [PATCH 013/154] Fix issues/pr list broken when there are many repositories (#8409) * fix issues/pr list broken when there are many repositories * remove unused codes * fix counting error on issues/prs * keep the old logic * fix panic * fix tests --- models/issue.go | 54 ++++++++++------- models/issue_test.go | 11 ++-- models/user.go | 59 +++++++----------- models/user_test.go | 22 ------- routers/user/home.go | 140 +++++++++++++++---------------------------- 5 files changed, 109 insertions(+), 177 deletions(-) diff --git a/models/issue.go b/models/issue.go index e4cc1291c2..cfa6191b47 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1306,18 +1306,19 @@ func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) { // IssuesOptions represents options of an issue. type IssuesOptions struct { - RepoIDs []int64 // include all repos if empty - AssigneeID int64 - PosterID int64 - MentionedID int64 - MilestoneID int64 - Page int - PageSize int - IsClosed util.OptionalBool - IsPull util.OptionalBool - LabelIDs []int64 - SortType string - IssueIDs []int64 + RepoIDs []int64 // include all repos if empty + RepoSubQuery *builder.Builder + AssigneeID int64 + PosterID int64 + MentionedID int64 + MilestoneID int64 + Page int + PageSize int + IsClosed util.OptionalBool + IsPull util.OptionalBool + LabelIDs []int64 + SortType string + IssueIDs []int64 } // sortIssuesSession sort an issues-related session based on the provided @@ -1360,7 +1361,9 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { sess.In("issue.id", opts.IssueIDs) } - if len(opts.RepoIDs) > 0 { + if opts.RepoSubQuery != nil { + sess.In("issue.repo_id", opts.RepoSubQuery) + } else if len(opts.RepoIDs) > 0 { // In case repository IDs are provided but actually no repository has issue. sess.In("issue.repo_id", opts.RepoIDs) } @@ -1627,12 +1630,12 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { // UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. type UserIssueStatsOptions struct { - UserID int64 - RepoID int64 - UserRepoIDs []int64 - FilterMode int - IsPull bool - IsClosed bool + UserID int64 + RepoID int64 + RepoSubQuery *builder.Builder + FilterMode int + IsPull bool + IsClosed bool } // GetUserIssueStats returns issue statistic information for dashboard by given conditions. @@ -1646,16 +1649,23 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID}) } + var repoCond = builder.NewCond() + if opts.RepoSubQuery != nil { + repoCond = builder.In("issue.repo_id", opts.RepoSubQuery) + } else { + repoCond = builder.Expr("0=1") + } + switch opts.FilterMode { case FilterModeAll: stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). - And(builder.In("issue.repo_id", opts.UserRepoIDs)). + And(repoCond). Count(new(Issue)) if err != nil { return nil, err } stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true). - And(builder.In("issue.repo_id", opts.UserRepoIDs)). + And(repoCond). Count(new(Issue)) if err != nil { return nil, err @@ -1730,7 +1740,7 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { } stats.YourRepositoriesCount, err = x.Where(cond). - And(builder.In("issue.repo_id", opts.UserRepoIDs)). + And(repoCond). Count(new(Issue)) if err != nil { return nil, err diff --git a/models/issue_test.go b/models/issue_test.go index 9cd9ff0ad9..65f4d6ba66 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "xorm.io/builder" ) func TestIssue_ReplaceLabels(t *testing.T) { @@ -266,10 +267,12 @@ func TestGetUserIssueStats(t *testing.T) { }, { UserIssueStatsOptions{ - UserID: 2, - UserRepoIDs: []int64{1, 2}, - FilterMode: FilterModeAll, - IsClosed: true, + UserID: 2, + RepoSubQuery: builder.Select("repository.id"). + From("repository"). + Where(builder.In("repository.id", []int64{1, 2})), + FilterMode: FilterModeAll, + IsClosed: true, }, IssueStats{ YourRepositoriesCount: 2, diff --git a/models/user.go b/models/user.go index 030e23c383..8c4befb139 100644 --- a/models/user.go +++ b/models/user.go @@ -615,50 +615,35 @@ func (u *User) GetRepositories(page, pageSize int) (err error) { return err } -// GetRepositoryIDs returns repositories IDs where user owned and has unittypes -func (u *User) GetRepositoryIDs(units ...UnitType) ([]int64, error) { - var ids []int64 - - sess := x.Table("repository").Cols("repository.id") +// UnitRepositoriesSubQuery returns repositories query builder according units +func (u *User) UnitRepositoriesSubQuery(units ...UnitType) *builder.Builder { + b := builder.Select("repository.id").From("repository") if len(units) > 0 { - sess = sess.Join("INNER", "repo_unit", "repository.id = repo_unit.repo_id") - sess = sess.In("repo_unit.type", units) + b.Join("INNER", "repo_unit", builder.Expr("repository.id = repo_unit.repo_id"). + And(builder.In("repo_unit.type", units)), + ) } - - return ids, sess.Where("owner_id = ?", u.ID).Find(&ids) + return b.Where(builder.Eq{"repository.owner_id": u.ID}) } -// GetOrgRepositoryIDs returns repositories IDs where user's team owned and has unittypes -func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) { - var ids []int64 - - sess := x.Table("repository"). - Cols("repository.id"). - Join("INNER", "team_user", "repository.owner_id = team_user.org_id"). - Join("INNER", "team_repo", "repository.is_private != ? OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true) - +// OrgUnitRepositoriesSubQuery returns repositories query builder according orgnizations and units +func (u *User) OrgUnitRepositoriesSubQuery(userID int64, units ...UnitType) *builder.Builder { + b := builder. + Select("team_repo.repo_id"). + From("team_repo"). + Join("INNER", "team_user", builder.Eq{"team_user.uid": userID}.And( + builder.Expr("team_user.team_id = team_repo.team_id"), + )) if len(units) > 0 { - sess = sess.Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id") - sess = sess.In("team_unit.type", units) + b.Join("INNER", "team_unit", builder.Eq{"team_unit.org_id": u.ID}.And( + builder.Expr("team_unit.team_id = team_repo.team_id").And( + builder.In("`type`", units), + ), + )) } - - return ids, sess. - Where("team_user.uid = ?", u.ID). - GroupBy("repository.id").Find(&ids) -} - -// GetAccessRepoIDs returns all repositories IDs where user's or user is a team member organizations -func (u *User) GetAccessRepoIDs(units ...UnitType) ([]int64, error) { - ids, err := u.GetRepositoryIDs(units...) - if err != nil { - return nil, err - } - ids2, err := u.GetOrgRepositoryIDs(units...) - if err != nil { - return nil, err - } - return append(ids, ids2...), nil + return b.Where(builder.Eq{"team_repo.org_id": u.ID}). + GroupBy("team_repo.repo_id") } // GetMirrorRepositories returns mirror repositories that user owns, including private repositories. diff --git a/models/user_test.go b/models/user_test.go index bcb955817c..75d806eadc 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -275,28 +275,6 @@ func BenchmarkHashPassword(b *testing.B) { } } -func TestGetOrgRepositoryIDs(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) - user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User) - user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) - - accessibleRepos, err := user2.GetOrgRepositoryIDs() - assert.NoError(t, err) - // User 2's team has access to private repos 3, 5, repo 32 is a public repo of the organization - assert.Equal(t, []int64{3, 5, 23, 24, 32}, accessibleRepos) - - accessibleRepos, err = user4.GetOrgRepositoryIDs() - assert.NoError(t, err) - // User 4's team has access to private repo 3, repo 32 is a public repo of the organization - assert.Equal(t, []int64{3, 32}, accessibleRepos) - - accessibleRepos, err = user5.GetOrgRepositoryIDs() - assert.NoError(t, err) - // User 5's team has no access to any repo - assert.Len(t, accessibleRepos, 0) -} - func TestNewGitSig(t *testing.T) { users := make([]*User, 0, 20) sess := x.NewSession() diff --git a/routers/user/home.go b/routers/user/home.go index 40b3bc3fc1..21fc62aae5 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -14,13 +14,11 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/keybase/go-crypto/openpgp" "github.com/keybase/go-crypto/openpgp/armor" - "github.com/unknwon/com" ) const ( @@ -152,6 +150,24 @@ func Dashboard(ctx *context.Context) { // Issues render the user issues page func Issues(ctx *context.Context) { isPullList := ctx.Params(":type") == "pulls" + repoID := ctx.QueryInt64("repo") + if repoID > 0 { + repo, err := models.GetRepositoryByID(repoID) + if err != nil { + ctx.ServerError("GetRepositoryByID", err) + return + } + perm, err := models.GetUserRepoPermission(repo, ctx.User) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + if !perm.CanReadIssuesOrPulls(isPullList) { + ctx.NotFound("Repository does not exist or you have no permission", nil) + return + } + } + if isPullList { ctx.Data["Title"] = ctx.Tr("pull_requests") ctx.Data["PageIsPulls"] = true @@ -194,58 +210,32 @@ func Issues(ctx *context.Context) { page = 1 } - repoID := ctx.QueryInt64("repo") - isShowClosed := ctx.Query("state") == "closed" + var ( + isShowClosed = ctx.Query("state") == "closed" + err error + opts = &models.IssuesOptions{ + IsClosed: util.OptionalBoolOf(isShowClosed), + IsPull: util.OptionalBoolOf(isPullList), + SortType: sortType, + } + ) // Get repositories. - var err error - var userRepoIDs []int64 - if ctxUser.IsOrganization() { - env, err := ctxUser.AccessibleReposEnv(ctx.User.ID) - if err != nil { - ctx.ServerError("AccessibleReposEnv", err) - return - } - userRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos) - if err != nil { - ctx.ServerError("env.RepoIDs", err) - return - } + if repoID > 0 { + opts.RepoIDs = []int64{repoID} } else { unitType := models.UnitTypeIssues if isPullList { unitType = models.UnitTypePullRequests } - userRepoIDs, err = ctxUser.GetAccessRepoIDs(unitType) - if err != nil { - ctx.ServerError("ctxUser.GetAccessRepoIDs", err) - return + if ctxUser.IsOrganization() { + opts.RepoSubQuery = ctxUser.OrgUnitRepositoriesSubQuery(ctx.User.ID, unitType) + } else { + opts.RepoSubQuery = ctxUser.UnitRepositoriesSubQuery(unitType) } } - if len(userRepoIDs) == 0 { - userRepoIDs = []int64{-1} - } - - opts := &models.IssuesOptions{ - IsClosed: util.OptionalBoolOf(isShowClosed), - IsPull: util.OptionalBoolOf(isPullList), - SortType: sortType, - } - - if repoID > 0 { - opts.RepoIDs = []int64{repoID} - } switch filterMode { - case models.FilterModeAll: - if repoID > 0 { - if !com.IsSliceContainsInt64(userRepoIDs, repoID) { - // force an empty result - opts.RepoIDs = []int64{-1} - } - } else { - opts.RepoIDs = userRepoIDs - } case models.FilterModeAssign: opts.AssigneeID = ctxUser.ID case models.FilterModeCreate: @@ -254,14 +244,6 @@ func Issues(ctx *context.Context) { opts.MentionedID = ctxUser.ID } - counts, err := models.CountIssuesByRepo(opts) - if err != nil { - ctx.ServerError("CountIssuesByRepo", err) - return - } - - opts.Page = page - opts.PageSize = setting.UI.IssuePagingNum var labelIDs []int64 selectLabels := ctx.Query("labels") if len(selectLabels) > 0 && selectLabels != "0" { @@ -273,6 +255,15 @@ func Issues(ctx *context.Context) { } opts.LabelIDs = labelIDs + counts, err := models.CountIssuesByRepo(opts) + if err != nil { + ctx.ServerError("CountIssuesByRepo", err) + return + } + + opts.Page = page + opts.PageSize = setting.UI.IssuePagingNum + issues, err := models.Issues(opts) if err != nil { ctx.ServerError("Issues", err) @@ -289,41 +280,6 @@ func Issues(ctx *context.Context) { showReposMap[repoID] = repo } - if repoID > 0 { - if _, ok := showReposMap[repoID]; !ok { - repo, err := models.GetRepositoryByID(repoID) - if models.IsErrRepoNotExist(err) { - ctx.NotFound("GetRepositoryByID", err) - return - } else if err != nil { - ctx.ServerError("GetRepositoryByID", fmt.Errorf("[%d]%v", repoID, err)) - return - } - showReposMap[repoID] = repo - } - - repo := showReposMap[repoID] - - // Check if user has access to given repository. - perm, err := models.GetUserRepoPermission(repo, ctxUser) - if err != nil { - ctx.ServerError("GetUserRepoPermission", fmt.Errorf("[%d]%v", repoID, err)) - return - } - if !perm.CanRead(models.UnitTypeIssues) { - if log.IsTrace() { - log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+ - "User in repo has Permissions: %-+v", - ctxUser, - models.UnitTypeIssues, - repo, - perm) - } - ctx.Status(404) - return - } - } - showRepos := models.RepositoryListOfMap(showReposMap) sort.Sort(showRepos) if err = showRepos.LoadAttributes(); err != nil { @@ -341,12 +297,12 @@ func Issues(ctx *context.Context) { } issueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{ - UserID: ctxUser.ID, - RepoID: repoID, - UserRepoIDs: userRepoIDs, - FilterMode: filterMode, - IsPull: isPullList, - IsClosed: isShowClosed, + UserID: ctxUser.ID, + RepoID: repoID, + RepoSubQuery: opts.RepoSubQuery, + FilterMode: filterMode, + IsPull: isPullList, + IsClosed: isShowClosed, }) if err != nil { ctx.ServerError("GetUserIssueStats", err) From 170743c8a0cdf216ee21076aadc5d905dfef0cd6 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 9 Oct 2019 01:55:16 +0800 Subject: [PATCH 014/154] Revert "Fix issues/pr list broken when there are many repositories (#8409)" (#8427) This reverts commit 78438d310be42f9c5e0e2937ee54e6050cc8f381. --- models/issue.go | 54 +++++++---------- models/issue_test.go | 11 ++-- models/user.go | 59 +++++++++++------- models/user_test.go | 22 +++++++ routers/user/home.go | 140 ++++++++++++++++++++++++++++--------------- 5 files changed, 177 insertions(+), 109 deletions(-) diff --git a/models/issue.go b/models/issue.go index cfa6191b47..e4cc1291c2 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1306,19 +1306,18 @@ func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) { // IssuesOptions represents options of an issue. type IssuesOptions struct { - RepoIDs []int64 // include all repos if empty - RepoSubQuery *builder.Builder - AssigneeID int64 - PosterID int64 - MentionedID int64 - MilestoneID int64 - Page int - PageSize int - IsClosed util.OptionalBool - IsPull util.OptionalBool - LabelIDs []int64 - SortType string - IssueIDs []int64 + RepoIDs []int64 // include all repos if empty + AssigneeID int64 + PosterID int64 + MentionedID int64 + MilestoneID int64 + Page int + PageSize int + IsClosed util.OptionalBool + IsPull util.OptionalBool + LabelIDs []int64 + SortType string + IssueIDs []int64 } // sortIssuesSession sort an issues-related session based on the provided @@ -1361,9 +1360,7 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { sess.In("issue.id", opts.IssueIDs) } - if opts.RepoSubQuery != nil { - sess.In("issue.repo_id", opts.RepoSubQuery) - } else if len(opts.RepoIDs) > 0 { + if len(opts.RepoIDs) > 0 { // In case repository IDs are provided but actually no repository has issue. sess.In("issue.repo_id", opts.RepoIDs) } @@ -1630,12 +1627,12 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) { // UserIssueStatsOptions contains parameters accepted by GetUserIssueStats. type UserIssueStatsOptions struct { - UserID int64 - RepoID int64 - RepoSubQuery *builder.Builder - FilterMode int - IsPull bool - IsClosed bool + UserID int64 + RepoID int64 + UserRepoIDs []int64 + FilterMode int + IsPull bool + IsClosed bool } // GetUserIssueStats returns issue statistic information for dashboard by given conditions. @@ -1649,23 +1646,16 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID}) } - var repoCond = builder.NewCond() - if opts.RepoSubQuery != nil { - repoCond = builder.In("issue.repo_id", opts.RepoSubQuery) - } else { - repoCond = builder.Expr("0=1") - } - switch opts.FilterMode { case FilterModeAll: stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false). - And(repoCond). + And(builder.In("issue.repo_id", opts.UserRepoIDs)). Count(new(Issue)) if err != nil { return nil, err } stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true). - And(repoCond). + And(builder.In("issue.repo_id", opts.UserRepoIDs)). Count(new(Issue)) if err != nil { return nil, err @@ -1740,7 +1730,7 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) { } stats.YourRepositoriesCount, err = x.Where(cond). - And(repoCond). + And(builder.In("issue.repo_id", opts.UserRepoIDs)). Count(new(Issue)) if err != nil { return nil, err diff --git a/models/issue_test.go b/models/issue_test.go index 65f4d6ba66..9cd9ff0ad9 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/stretchr/testify/assert" - "xorm.io/builder" ) func TestIssue_ReplaceLabels(t *testing.T) { @@ -267,12 +266,10 @@ func TestGetUserIssueStats(t *testing.T) { }, { UserIssueStatsOptions{ - UserID: 2, - RepoSubQuery: builder.Select("repository.id"). - From("repository"). - Where(builder.In("repository.id", []int64{1, 2})), - FilterMode: FilterModeAll, - IsClosed: true, + UserID: 2, + UserRepoIDs: []int64{1, 2}, + FilterMode: FilterModeAll, + IsClosed: true, }, IssueStats{ YourRepositoriesCount: 2, diff --git a/models/user.go b/models/user.go index 8c4befb139..030e23c383 100644 --- a/models/user.go +++ b/models/user.go @@ -615,35 +615,50 @@ func (u *User) GetRepositories(page, pageSize int) (err error) { return err } -// UnitRepositoriesSubQuery returns repositories query builder according units -func (u *User) UnitRepositoriesSubQuery(units ...UnitType) *builder.Builder { - b := builder.Select("repository.id").From("repository") +// GetRepositoryIDs returns repositories IDs where user owned and has unittypes +func (u *User) GetRepositoryIDs(units ...UnitType) ([]int64, error) { + var ids []int64 + + sess := x.Table("repository").Cols("repository.id") if len(units) > 0 { - b.Join("INNER", "repo_unit", builder.Expr("repository.id = repo_unit.repo_id"). - And(builder.In("repo_unit.type", units)), - ) + sess = sess.Join("INNER", "repo_unit", "repository.id = repo_unit.repo_id") + sess = sess.In("repo_unit.type", units) } - return b.Where(builder.Eq{"repository.owner_id": u.ID}) + + return ids, sess.Where("owner_id = ?", u.ID).Find(&ids) } -// OrgUnitRepositoriesSubQuery returns repositories query builder according orgnizations and units -func (u *User) OrgUnitRepositoriesSubQuery(userID int64, units ...UnitType) *builder.Builder { - b := builder. - Select("team_repo.repo_id"). - From("team_repo"). - Join("INNER", "team_user", builder.Eq{"team_user.uid": userID}.And( - builder.Expr("team_user.team_id = team_repo.team_id"), - )) +// GetOrgRepositoryIDs returns repositories IDs where user's team owned and has unittypes +func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) { + var ids []int64 + + sess := x.Table("repository"). + Cols("repository.id"). + Join("INNER", "team_user", "repository.owner_id = team_user.org_id"). + Join("INNER", "team_repo", "repository.is_private != ? OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true) + if len(units) > 0 { - b.Join("INNER", "team_unit", builder.Eq{"team_unit.org_id": u.ID}.And( - builder.Expr("team_unit.team_id = team_repo.team_id").And( - builder.In("`type`", units), - ), - )) + sess = sess.Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id") + sess = sess.In("team_unit.type", units) } - return b.Where(builder.Eq{"team_repo.org_id": u.ID}). - GroupBy("team_repo.repo_id") + + return ids, sess. + Where("team_user.uid = ?", u.ID). + GroupBy("repository.id").Find(&ids) +} + +// GetAccessRepoIDs returns all repositories IDs where user's or user is a team member organizations +func (u *User) GetAccessRepoIDs(units ...UnitType) ([]int64, error) { + ids, err := u.GetRepositoryIDs(units...) + if err != nil { + return nil, err + } + ids2, err := u.GetOrgRepositoryIDs(units...) + if err != nil { + return nil, err + } + return append(ids, ids2...), nil } // GetMirrorRepositories returns mirror repositories that user owns, including private repositories. diff --git a/models/user_test.go b/models/user_test.go index 75d806eadc..bcb955817c 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -275,6 +275,28 @@ func BenchmarkHashPassword(b *testing.B) { } } +func TestGetOrgRepositoryIDs(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) + user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User) + user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) + + accessibleRepos, err := user2.GetOrgRepositoryIDs() + assert.NoError(t, err) + // User 2's team has access to private repos 3, 5, repo 32 is a public repo of the organization + assert.Equal(t, []int64{3, 5, 23, 24, 32}, accessibleRepos) + + accessibleRepos, err = user4.GetOrgRepositoryIDs() + assert.NoError(t, err) + // User 4's team has access to private repo 3, repo 32 is a public repo of the organization + assert.Equal(t, []int64{3, 32}, accessibleRepos) + + accessibleRepos, err = user5.GetOrgRepositoryIDs() + assert.NoError(t, err) + // User 5's team has no access to any repo + assert.Len(t, accessibleRepos, 0) +} + func TestNewGitSig(t *testing.T) { users := make([]*User, 0, 20) sess := x.NewSession() diff --git a/routers/user/home.go b/routers/user/home.go index 21fc62aae5..40b3bc3fc1 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -14,11 +14,13 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/keybase/go-crypto/openpgp" "github.com/keybase/go-crypto/openpgp/armor" + "github.com/unknwon/com" ) const ( @@ -150,24 +152,6 @@ func Dashboard(ctx *context.Context) { // Issues render the user issues page func Issues(ctx *context.Context) { isPullList := ctx.Params(":type") == "pulls" - repoID := ctx.QueryInt64("repo") - if repoID > 0 { - repo, err := models.GetRepositoryByID(repoID) - if err != nil { - ctx.ServerError("GetRepositoryByID", err) - return - } - perm, err := models.GetUserRepoPermission(repo, ctx.User) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return - } - if !perm.CanReadIssuesOrPulls(isPullList) { - ctx.NotFound("Repository does not exist or you have no permission", nil) - return - } - } - if isPullList { ctx.Data["Title"] = ctx.Tr("pull_requests") ctx.Data["PageIsPulls"] = true @@ -210,32 +194,58 @@ func Issues(ctx *context.Context) { page = 1 } - var ( - isShowClosed = ctx.Query("state") == "closed" - err error - opts = &models.IssuesOptions{ - IsClosed: util.OptionalBoolOf(isShowClosed), - IsPull: util.OptionalBoolOf(isPullList), - SortType: sortType, - } - ) + repoID := ctx.QueryInt64("repo") + isShowClosed := ctx.Query("state") == "closed" // Get repositories. - if repoID > 0 { - opts.RepoIDs = []int64{repoID} + var err error + var userRepoIDs []int64 + if ctxUser.IsOrganization() { + env, err := ctxUser.AccessibleReposEnv(ctx.User.ID) + if err != nil { + ctx.ServerError("AccessibleReposEnv", err) + return + } + userRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos) + if err != nil { + ctx.ServerError("env.RepoIDs", err) + return + } } else { unitType := models.UnitTypeIssues if isPullList { unitType = models.UnitTypePullRequests } - if ctxUser.IsOrganization() { - opts.RepoSubQuery = ctxUser.OrgUnitRepositoriesSubQuery(ctx.User.ID, unitType) - } else { - opts.RepoSubQuery = ctxUser.UnitRepositoriesSubQuery(unitType) + userRepoIDs, err = ctxUser.GetAccessRepoIDs(unitType) + if err != nil { + ctx.ServerError("ctxUser.GetAccessRepoIDs", err) + return } } + if len(userRepoIDs) == 0 { + userRepoIDs = []int64{-1} + } + + opts := &models.IssuesOptions{ + IsClosed: util.OptionalBoolOf(isShowClosed), + IsPull: util.OptionalBoolOf(isPullList), + SortType: sortType, + } + + if repoID > 0 { + opts.RepoIDs = []int64{repoID} + } switch filterMode { + case models.FilterModeAll: + if repoID > 0 { + if !com.IsSliceContainsInt64(userRepoIDs, repoID) { + // force an empty result + opts.RepoIDs = []int64{-1} + } + } else { + opts.RepoIDs = userRepoIDs + } case models.FilterModeAssign: opts.AssigneeID = ctxUser.ID case models.FilterModeCreate: @@ -244,6 +254,14 @@ func Issues(ctx *context.Context) { opts.MentionedID = ctxUser.ID } + counts, err := models.CountIssuesByRepo(opts) + if err != nil { + ctx.ServerError("CountIssuesByRepo", err) + return + } + + opts.Page = page + opts.PageSize = setting.UI.IssuePagingNum var labelIDs []int64 selectLabels := ctx.Query("labels") if len(selectLabels) > 0 && selectLabels != "0" { @@ -255,15 +273,6 @@ func Issues(ctx *context.Context) { } opts.LabelIDs = labelIDs - counts, err := models.CountIssuesByRepo(opts) - if err != nil { - ctx.ServerError("CountIssuesByRepo", err) - return - } - - opts.Page = page - opts.PageSize = setting.UI.IssuePagingNum - issues, err := models.Issues(opts) if err != nil { ctx.ServerError("Issues", err) @@ -280,6 +289,41 @@ func Issues(ctx *context.Context) { showReposMap[repoID] = repo } + if repoID > 0 { + if _, ok := showReposMap[repoID]; !ok { + repo, err := models.GetRepositoryByID(repoID) + if models.IsErrRepoNotExist(err) { + ctx.NotFound("GetRepositoryByID", err) + return + } else if err != nil { + ctx.ServerError("GetRepositoryByID", fmt.Errorf("[%d]%v", repoID, err)) + return + } + showReposMap[repoID] = repo + } + + repo := showReposMap[repoID] + + // Check if user has access to given repository. + perm, err := models.GetUserRepoPermission(repo, ctxUser) + if err != nil { + ctx.ServerError("GetUserRepoPermission", fmt.Errorf("[%d]%v", repoID, err)) + return + } + if !perm.CanRead(models.UnitTypeIssues) { + if log.IsTrace() { + log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+ + "User in repo has Permissions: %-+v", + ctxUser, + models.UnitTypeIssues, + repo, + perm) + } + ctx.Status(404) + return + } + } + showRepos := models.RepositoryListOfMap(showReposMap) sort.Sort(showRepos) if err = showRepos.LoadAttributes(); err != nil { @@ -297,12 +341,12 @@ func Issues(ctx *context.Context) { } issueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{ - UserID: ctxUser.ID, - RepoID: repoID, - RepoSubQuery: opts.RepoSubQuery, - FilterMode: filterMode, - IsPull: isPullList, - IsClosed: isShowClosed, + UserID: ctxUser.ID, + RepoID: repoID, + UserRepoIDs: userRepoIDs, + FilterMode: filterMode, + IsPull: isPullList, + IsClosed: isShowClosed, }) if err != nil { ctx.ServerError("GetUserIssueStats", err) From 736ad8f091696371ac15c9631e0f95b4073b23f5 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 8 Oct 2019 18:23:11 +0000 Subject: [PATCH 015/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 1 + options/locale/locale_pt-BR.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index fcec489af8..c038444f62 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -680,6 +680,7 @@ 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 diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 76890ddc79..f38380bb48 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -680,6 +680,7 @@ 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 From 4843723d001a7442db46a205e8a41d38e7e0d7c9 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Tue, 8 Oct 2019 16:18:17 -0300 Subject: [PATCH 016/154] Allow users with explicit read access to give approvals (#8382) --- models/branches.go | 23 ++++++++++++++++++++++- models/repo.go | 13 +++++++++++++ routers/repo/setting_protected_branch.go | 4 ++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/models/branches.go b/models/branches.go index 9daaa487e7..fa8beb866c 100644 --- a/models/branches.go +++ b/models/branches.go @@ -195,7 +195,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 +301,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/repo.go b/models/repo.go index 69f32ae4a5..8b3784bae0 100644 --- a/models/repo.go +++ b/models/repo.go @@ -735,11 +735,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 { diff --git a/routers/repo/setting_protected_branch.go b/routers/repo/setting_protected_branch.go index 80f44ead99..2a8502e6f4 100644 --- a/routers/repo/setting_protected_branch.go +++ b/routers/repo/setting_protected_branch.go @@ -117,9 +117,9 @@ func SettingsProtectedBranch(c *context.Context) { } } - users, err := c.Repo.Repository.GetWriters() + users, err := c.Repo.Repository.GetReaders() if err != nil { - c.ServerError("Repo.Repository.GetWriters", err) + c.ServerError("Repo.Repository.GetReaders", err) return } c.Data["Users"] = users From 4fe04f1adcd7897d4db3beee69d62d3ccdbd18da Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 8 Oct 2019 19:20:34 +0000 Subject: [PATCH 017/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_tr-TR.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 9fa1b7d42b..9c06ba326f 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -634,6 +634,8 @@ stored_lfs=Git LFS ile depolandı commit_graph=İşleme Grafiği blame=Bahset normal_view=Normal Görünüm +line=satır +lines=satır editor.new_file=Yeni dosya editor.upload_file=Dosya Yükle From f05a3353f4788ade6156bf132796690cf3154fd3 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Tue, 8 Oct 2019 16:48:57 -0300 Subject: [PATCH 018/154] Update strk.kbt.io/projects/go/libravatar to latest; closes #7860 (#8429) --- go.mod | 2 +- go.sum | 4 +- vendor/modules.txt | 2 +- .../projects/go/libravatar/Makefile | 12 ++++ .../projects/go/libravatar/README.md | 6 +- .../projects/go/libravatar/libravatar.go | 72 +++++++++++-------- 6 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 vendor/strk.kbt.io/projects/go/libravatar/Makefile diff --git a/go.mod b/go.mod index e1a2b7b404..d2520da739 100644 --- a/go.mod +++ b/go.mod @@ -121,7 +121,7 @@ require ( 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 + strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 xorm.io/builder v0.3.6 xorm.io/core v0.7.2 ) diff --git a/go.sum b/go.sum index c068caa2f7..3adf91e926 100644 --- a/go.sum +++ b/go.sum @@ -815,8 +815,8 @@ honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt 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= 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= diff --git a/vendor/modules.txt b/vendor/modules.txt index 71ba274867..8b2224f54a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -612,7 +612,7 @@ gopkg.in/warnings.v0 gopkg.in/yaml.v2 # mvdan.cc/xurls/v2 v2.0.0 mvdan.cc/xurls/v2 -# strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a +# strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 strk.kbt.io/projects/go/libravatar # xorm.io/builder v0.3.6 xorm.io/builder diff --git a/vendor/strk.kbt.io/projects/go/libravatar/Makefile b/vendor/strk.kbt.io/projects/go/libravatar/Makefile new file mode 100644 index 0000000000..2a70ddc72c --- /dev/null +++ b/vendor/strk.kbt.io/projects/go/libravatar/Makefile @@ -0,0 +1,12 @@ +PACKAGES ?= $(shell go list ./...) + +.PHONY: check +check: lint + go test + +.PHONY: lint +lint: + @which golint > /dev/null; if [ $$? -ne 0 ]; then \ + go get -u github.com/golang/lint/golint; \ + fi + @for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; diff --git a/vendor/strk.kbt.io/projects/go/libravatar/README.md b/vendor/strk.kbt.io/projects/go/libravatar/README.md index 660cd90643..c0a9f942e2 100644 --- a/vendor/strk.kbt.io/projects/go/libravatar/README.md +++ b/vendor/strk.kbt.io/projects/go/libravatar/README.md @@ -1,12 +1,14 @@ Simple [golang](https://www.golang.org) library for serving [federated avatars](https://www.libravatar.org) +[![trunk](https://goreportcard.com/badge/strk.kbt.io/projects/go/libravatar)] +(https://goreportcard.com/report/strk.kbt.io/projects/go/libravatar) + # Use ```sh go get strk.kbt.io/projects/go/libravatar -cd $GOPATH/src/strk.kbt.io/projects/go/libravatar -go doc +go doc strk.kbt.io/projects/go/libravatar ``` # Contribute diff --git a/vendor/strk.kbt.io/projects/go/libravatar/libravatar.go b/vendor/strk.kbt.io/projects/go/libravatar/libravatar.go index 4c748456f3..fe544cd4b4 100644 --- a/vendor/strk.kbt.io/projects/go/libravatar/libravatar.go +++ b/vendor/strk.kbt.io/projects/go/libravatar/libravatar.go @@ -5,7 +5,7 @@ // Implements support for federated avatars lookup. // See https://wiki.libravatar.org/api/ -package libravatar +package libravatar // import "strk.kbt.io/projects/go/libravatar" import ( "crypto/md5" @@ -16,6 +16,7 @@ import ( "net/mail" "net/url" "strings" + "sync" "time" ) @@ -38,7 +39,8 @@ const ( ) var ( - // Default object, enabling object-less function calls + // DefaultLibravatar is a default Libravatar object, + // enabling object-less function calls DefaultLibravatar = New() ) @@ -53,14 +55,16 @@ type cacheValue struct { checkedAt time.Time } +// Libravatar is an opaque structure holding service configuration type Libravatar struct { - defUrl string // default url + defURL string // default url picSize int // picture size fallbackHost string // default fallback URL secureFallbackHost string // default fallback URL for secure connections useHTTPS bool nameCache map[cacheKey]cacheValue nameCacheDuration time.Duration + nameCacheMutex *sync.Mutex minSize uint // smallest image dimension allowed maxSize uint // largest image dimension allowed size uint // what dimension should be used @@ -68,7 +72,7 @@ type Libravatar struct { secureServiceBase string // SRV record to be queried for federation with secure servers } -// Instanciate a library handle +// New instanciates a new Libravatar object (handle) func New() *Libravatar { // According to https://wiki.libravatar.org/running_your_own/ // the time-to-live (cache expiry) should be set to at least 1 day. @@ -82,27 +86,28 @@ func New() *Libravatar { secureServiceBase: `avatars-sec`, nameCache: make(map[cacheKey]cacheValue), nameCacheDuration: 24 * time.Hour, + nameCacheMutex: &sync.Mutex{}, } } -// Set the hostname for fallbacks in case no avatar service is defined -// for a domain +// SetFallbackHost sets the hostname for fallbacks in case no avatar +// service is defined for a domain func (v *Libravatar) SetFallbackHost(host string) { v.fallbackHost = host } -// Set the hostname for fallbacks in case no avatar service is defined -// for a domain, when requiring secure domains +// SetSecureFallbackHost sets the hostname for fallbacks in case no +// avatar service is defined for a domain, when requiring secure domains func (v *Libravatar) SetSecureFallbackHost(host string) { v.secureFallbackHost = host } -// Set useHTTPS flag +// SetUseHTTPS sets flag requesting use of https for fetching avatars func (v *Libravatar) SetUseHTTPS(use bool) { v.useHTTPS = use } -// Set Avatars image dimension (0 for default) +// SetAvatarSize sets avatars image dimension (0 for default) func (v *Libravatar) SetAvatarSize(size uint) { v.size = size } @@ -150,8 +155,8 @@ func (v *Libravatar) process(email *mail.Address, openid *url.URL) (string, erro res := fmt.Sprintf("%s/avatar/%s", URL, v.genHash(email, openid)) values := make(url.Values) - if v.defUrl != "" { - values.Add("d", v.defUrl) + if v.defURL != "" { + values.Add("d", v.defURL) } if v.size > 0 { values.Add("s", fmt.Sprintf("%d", v.size)) @@ -181,7 +186,9 @@ func (v *Libravatar) baseURL(email *mail.Address, openid *url.URL) (string, erro host := v.getDomain(email, openid) key := cacheKey{service, host} now := time.Now() + v.nameCacheMutex.Lock() val, found := v.nameCache[key] + v.nameCacheMutex.Unlock() if found && now.Sub(val.checkedAt) <= v.nameCacheDuration { return protocol + val.target, nil } @@ -204,53 +211,55 @@ func (v *Libravatar) baseURL(email *mail.Address, openid *url.URL) (string, erro } var ( - total_weight uint16 - records []record - top_priority = addrs[0].Priority - top_record *net.SRV + totalWeight uint16 + records []record + topPriority = addrs[0].Priority + topRecord *net.SRV ) for _, rr := range addrs { - if rr.Priority > top_priority { + if rr.Priority > topPriority { continue - } else if rr.Priority < top_priority { + } else if rr.Priority < topPriority { // won't happen, because net sorts // by priority, but just in case - total_weight = 0 + totalWeight = 0 records = nil - top_priority = rr.Priority + topPriority = rr.Priority } - total_weight += rr.Weight + totalWeight += rr.Weight if rr.Weight > 0 { - records = append(records, record{rr, total_weight}) + records = append(records, record{rr, totalWeight}) } else if rr.Weight == 0 { - records = append([]record{record{srv: rr, weight: total_weight}}, records...) + records = append([]record{record{srv: rr, weight: totalWeight}}, records...) } } if len(records) == 1 { - top_record = records[0].srv + topRecord = records[0].srv } else { - randnum := uint16(rand.Intn(int(total_weight))) + randnum := uint16(rand.Intn(int(totalWeight))) for _, rr := range records { if rr.weight >= randnum { - top_record = rr.srv + topRecord = rr.srv break } } } - domain = fmt.Sprintf("%s:%d", top_record.Target, top_record.Port) + domain = fmt.Sprintf("%s:%d", topRecord.Target, topRecord.Port) } + v.nameCacheMutex.Lock() v.nameCache[key] = cacheValue{checkedAt: now, target: domain} + v.nameCacheMutex.Unlock() return protocol + domain, nil } -// Return url of the avatar for the given email +// FromEmail returns the url of the avatar for the given email func (v *Libravatar) FromEmail(email string) (string, error) { addr, err := mail.ParseAddress(email) if err != nil { @@ -265,12 +274,13 @@ func (v *Libravatar) FromEmail(email string) (string, error) { return link, nil } -// Object-less call to DefaultLibravatar for an email adders +// FromEmail is the object-less call to DefaultLibravatar for an email adders func FromEmail(email string) (string, error) { return DefaultLibravatar.FromEmail(email) } -// Return url of the avatar for the given url (typically for OpenID) +// FromURL returns the url of the avatar for the given url (typically +// for OpenID) func (v *Libravatar) FromURL(openid string) (string, error) { ourl, err := url.Parse(openid) if err != nil { @@ -291,7 +301,7 @@ func (v *Libravatar) FromURL(openid string) (string, error) { return link, nil } -// Object-less call to DefaultLibravatar for a URL +// FromURL is the object-less call to DefaultLibravatar for a URL func FromURL(openid string) (string, error) { return DefaultLibravatar.FromURL(openid) } From 7408942c802cd11c0fe4b9498a0de26964893cfd Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 8 Oct 2019 22:42:30 +0200 Subject: [PATCH 019/154] Update golangci to v1.20 (#8432) * Update golangci to v1.20 Signed-off-by: kolaente * Use the timeout flag instead of deadline, move it to config Signed-off-by: kolaente --- .golangci.yml | 3 +++ Makefile | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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/Makefile b/Makefile index e8220ef36c..953abe83b0 100644 --- a/Makefile +++ b/Makefile @@ -515,6 +515,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.19.1; \ + 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 From a3612f9d35811e41bd0e7387dce86a6ddb099f49 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Tue, 8 Oct 2019 22:27:45 -0300 Subject: [PATCH 020/154] Changelog for v1.9.4 (#8422) (#8433) * changelog * Update CHANGELOG.md We ned to revert this then ... Co-Authored-By: Lauris BH --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 540b1a9790..9c71af6b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ 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.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) From 3810fa48ac46620432fbf91571a01eeea0b460b3 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 9 Oct 2019 01:29:16 +0000 Subject: [PATCH 021/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index ca38757e1c..9fe31ec5ba 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -680,6 +680,8 @@ stored_lfs=Git LFSで保管されています commit_graph=コミットグラフ blame=Blame normal_view=通常表示 +line=行 +lines=行 editor.new_file=新規ファイル editor.upload_file=ファイルをアップロード @@ -705,6 +707,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=キャンセル From dd611c9a86f5a192a7a76146613a7e087b09f221 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Wed, 9 Oct 2019 06:36:53 -0300 Subject: [PATCH 022/154] Fix migration v96 to keep issue attachments (#8435) * Fix migration v96 to keep issue attachments * Fix == operator --- models/migrations/v96.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/migrations/v96.go b/models/migrations/v96.go index 5c2135ffc6..34f67534c2 100644 --- a/models/migrations/v96.go +++ b/models/migrations/v96.go @@ -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) From 7ad46cc116e4749a0d45572f1a8c53d0c8729080 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 9 Oct 2019 21:09:02 +0800 Subject: [PATCH 023/154] fix template bug on mirror repository setting page (#8438) --- modules/templates/helper.go | 2 ++ templates/repo/settings/options.tmpl | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index b40f7117f5..2c53e05fca 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -236,6 +236,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/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 4f7d32479e..a93efb8d25 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -88,15 +88,15 @@ -
+
- +
- +
From b6616591d190da56ba1ce06570f38a6cca3821d6 Mon Sep 17 00:00:00 2001 From: Tekaoh <45337851+Tekaoh@users.noreply.github.com> Date: Wed, 9 Oct 2019 13:49:37 -0500 Subject: [PATCH 024/154] Check for either escaped or unescaped wiki filenames (#8408) * Check for either escaped or unescaped wiki filenames + Gitea currently saves wiki pages with escaped filenames. + Wikis mirrored from other places like Github use unescaped filenames. + We need to be checking for filenames in either format to increase compatibility. * Better logic for escaped and unescaped wiki filenames Co-Authored-By: null --- routers/repo/wiki.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/routers/repo/wiki.go b/routers/repo/wiki.go index bf8ac658ae..02fbe4a1dd 100644 --- a/routers/repo/wiki.go +++ b/routers/repo/wiki.go @@ -8,6 +8,7 @@ package repo import ( "fmt" "io/ioutil" + "net/url" "path/filepath" "strings" @@ -68,11 +69,22 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) if err != nil { return nil, err } + // The longest name should be checked first for _, entry := range entries { if entry.IsRegular() && entry.Name() == target { return entry, nil } } + // Then the unescaped, shortest alternative + var unescapedTarget string + if unescapedTarget, err = url.QueryUnescape(target); err != nil { + return nil, err + } + for _, entry := range entries { + if entry.IsRegular() && entry.Name() == unescapedTarget { + return entry, nil + } + } return nil, nil } From 5109d18b298235c09e75eb0d3e900e777ac786b2 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Wed, 9 Oct 2019 19:02:21 +0000 Subject: [PATCH 025/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_es-ES.ini | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 675de019b0..dbfa462217 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -299,6 +299,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. @@ -317,6 +318,7 @@ 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,6 +558,10 @@ 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 @@ -572,6 +578,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 +592,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 @@ -670,6 +679,7 @@ stored_lfs=Almacenados con Git LFS commit_graph=Gráfico de commits blame=Blame normal_view=Vista normal +lines=líneas editor.new_file=Nuevo Archivo editor.upload_file=Subir archivo @@ -695,6 +705,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 @@ -769,6 +780,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 +837,7 @@ 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.poster=Autor issues.collaborator=Colaborador issues.owner=Propietario @@ -1116,6 +1129,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 +1137,7 @@ 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.site=Sitio web settings.update_settings=Actualizar configuración settings.advanced_settings=Ajustes avanzados From e270896a834f9f73c25b3e27fe596d2ced55b414 Mon Sep 17 00:00:00 2001 From: 8ctopus <13252042+8ctopus@users.noreply.github.com> Date: Thu, 10 Oct 2019 03:33:03 +0500 Subject: [PATCH 026/154] Doc updated list of supported webhooks and added example (#8388) * Doc updated list of supported webhooks and added example * Replaced webhook password verification by signature verification --- docs/content/doc/features/webhooks.en-us.md | 82 ++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) 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 + +``` + Date: Thu, 10 Oct 2019 04:11:25 +0500 Subject: [PATCH 027/154] =?UTF-8?q?Doc=20recommend=20to=20use=20reverse=20?= =?UTF-8?q?proxy=20if=20Apache/nginx=20is=20also=20running=20on=E2=80=A6?= =?UTF-8?q?=20(#8384)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Doc recommend to use reverse proxy if Apache/nginx is also running on server * Update docs/content/doc/usage/https-support.md Co-Authored-By: John Olheiser <42128690+jolheiser@users.noreply.github.com> --- docs/content/doc/usage/https-support.md | 2 ++ 1 file changed, 2 insertions(+) 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 From 1fe81bc22edbb5c82dd2b1518fb9337c1bd503bb Mon Sep 17 00:00:00 2001 From: 6543 <24977596+6543@users.noreply.github.com> Date: Thu, 10 Oct 2019 04:16:58 +0200 Subject: [PATCH 028/154] add crowdin badge (#8447) --- README.md | 1 + README_ZH.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 5d565e4a3a..76feebdbe7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ [![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=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 b546a15a3e..0d9d6d27da 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -11,6 +11,7 @@ [![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=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) ## 目标 From eac5a8be751341b388cf16c5a43aeb2f83c8ed2c Mon Sep 17 00:00:00 2001 From: pseudocoder Date: Thu, 10 Oct 2019 15:42:01 +0300 Subject: [PATCH 029/154] DOCS: add mention of swagger api reference (#8452) It's(swagger api link) mentioned vaguely in the FAQ but IMHO missing from API usage page. --- docs/content/doc/advanced/api-usage.en-us.md | 8 ++++++++ 1 file changed, 8 insertions(+) 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 From 7c1ddd5692cd62a208886276196f02422dbead95 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 10 Oct 2019 12:44:06 +0000 Subject: [PATCH 030/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_tr-TR.ini | 8 ++++++++ options/locale/locale_zh-CN.ini | 3 +++ 2 files changed, 11 insertions(+) diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 9c06ba326f..b387bf2e80 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -768,6 +768,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ı @@ -837,11 +839,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` @@ -862,6 +868,8 @@ 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=eklenen bitiş tarihi %s %s +issues.due_date_remove=kaldırılan bitiş tarihi %s %s 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. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 7667609793..7b62bf9c12 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -680,6 +680,8 @@ stored_lfs=存储到Git LFS commit_graph=提交图 blame=Blame normal_view=普通视图 +line=行 +lines=行 editor.new_file=新建文件 editor.upload_file=上传文件 @@ -705,6 +707,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=取消 From 57b0d9a38ba7d8dcc05a74fe39ab9f9e765ed8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C5=9Far=20=C3=87iv?= <42207558+yasarciv67@users.noreply.github.com> Date: Thu, 10 Oct 2019 16:47:38 +0300 Subject: [PATCH 031/154] Add @yasarciv67 to TRANSLATORS file (#8451) --- options/locale/TRANSLATORS | 1 + 1 file changed, 1 insertion(+) 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 From df2c11a878719719b8600745888c570af93827be Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Thu, 10 Oct 2019 13:45:11 -0300 Subject: [PATCH 032/154] Ignore mentions for users with no access (#8395) * Draft for ResolveMentionsByVisibility() * Correct typo * Resolve teams instead of orgs for mentions * Create test for ResolveMentionsByVisibility * Fix check for individual users and doer * Test and fix team mentions * Run all mentions through visibility filter * Fix error check * Simplify code, fix doer included in teams * Simplify team id list build --- models/issue.go | 155 +++++++++++++++++++++++++------- models/issue_test.go | 32 +++++++ models/org_team.go | 2 +- services/mailer/mail_comment.go | 13 ++- services/mailer/mail_issue.go | 13 ++- 5 files changed, 175 insertions(+), 40 deletions(-) diff --git a/models/issue.go b/models/issue.go index e4cc1291c2..f8fa1377a8 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1477,46 +1477,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 +1881,120 @@ 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 +} diff --git a/models/issue_test.go b/models/issue_test.go index 9cd9ff0ad9..5b039bc1d5 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -366,3 +366,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/org_team.go b/models/org_team.go index fc5d5834ef..9170ea2c2a 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -314,7 +314,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 { diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go index cb477f887b..f64db04fff 100644 --- a/services/mailer/mail_comment.go +++ b/services/mailer/mail_comment.go @@ -19,11 +19,18 @@ func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue } func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType models.ActionType, issue *models.Issue) (err error) { - mentions := markup.FindAllMentions(c.Content) - if err = models.UpdateIssueMentions(ctx, c.IssueID, mentions); err != nil { + rawMentions := markup.FindAllMentions(c.Content) + userMentions, err := issue.ResolveMentionsByVisibility(ctx, c.Poster, rawMentions) + if err != nil { + return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", c.IssueID, err) + } + if err = models.UpdateIssueMentions(ctx, c.IssueID, userMentions); err != nil { return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err) } - + mentions := make([]string, len(userMentions)) + for i, u := range userMentions { + mentions[i] = u.LowerName + } if len(c.Content) > 0 { if err = mailIssueCommentToParticipants(issue, c.Poster, c.Content, c, mentions); err != nil { log.Error("mailIssueCommentToParticipants: %v", err) diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index 92d2c5a879..da0249d595 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -123,11 +123,18 @@ func MailParticipants(issue *models.Issue, doer *models.User, opType models.Acti } func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.User, opType models.ActionType) (err error) { - mentions := markup.FindAllMentions(issue.Content) - - if err = models.UpdateIssueMentions(ctx, issue.ID, mentions); err != nil { + rawMentions := markup.FindAllMentions(issue.Content) + userMentions, err := issue.ResolveMentionsByVisibility(ctx, doer, rawMentions) + if err != nil { + return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", issue.ID, err) + } + if err = models.UpdateIssueMentions(ctx, issue.ID, userMentions); err != nil { return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) } + mentions := make([]string, len(userMentions)) + for i, u := range userMentions { + mentions[i] = u.LowerName + } if len(issue.Content) > 0 { if err = mailIssueCommentToParticipants(issue, doer, issue.Content, nil, mentions); err != nil { From 6551a9d6ca8ab79fe1460eb9d60a5a0e76110eb3 Mon Sep 17 00:00:00 2001 From: zeripath Date: Thu, 10 Oct 2019 18:42:28 +0100 Subject: [PATCH 033/154] Ensure Request Body Readers are closed in LFS server (#8454) --- modules/lfs/locks.go | 8 ++++++-- modules/lfs/server.go | 12 +++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) 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 From 9ff9f5ad1d8d2680c9c146831458afdbd4e641df Mon Sep 17 00:00:00 2001 From: zeripath Date: Thu, 10 Oct 2019 22:08:33 +0100 Subject: [PATCH 034/154] Ensure that LFS files are relative to the LFS content path (#8455) --- models/repo.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models/repo.go b/models/repo.go index 8b3784bae0..50402c8e1a 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1946,12 +1946,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) } From 46a12f196b1742a2462259ed3dd9c33c4c2f150b Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 11 Oct 2019 14:44:43 +0800 Subject: [PATCH 035/154] Move change issue title from models to issue service package (#8456) * move change issue title from models to issue service package * make the change less * fix typo --- integrations/api_pull_test.go | 3 +- models/issue.go | 57 ++--------------------------------- models/issue_lock.go | 17 ++++++++--- models/issue_test.go | 2 +- routers/repo/issue.go | 2 +- services/issue/issue.go | 51 +++++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 61 deletions(-) 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/models/issue.go b/models/issue.go index f8fa1377a8..8ce7d496ab 100644 --- a/models/issue.go +++ b/models/issue.go @@ -714,11 +714,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) @@ -844,9 +839,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 +855,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 +867,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 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_test.go b/models/issue_test.go index 5b039bc1d5..0be3f68808 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -160,7 +160,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) diff --git a/routers/repo/issue.go b/routers/repo/issue.go index e4ef3b1dbb..16a049c7aa 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -1044,7 +1044,7 @@ func UpdateIssueTitle(ctx *context.Context) { } oldTitle := issue.Title - if err := issue.ChangeTitle(ctx.User, title); err != nil { + if err := issue_service.ChangeTitle(issue, ctx.User, title); err != nil { ctx.ServerError("ChangeTitle", err) return } diff --git a/services/issue/issue.go b/services/issue/issue.go index 5afdfc9901..a28916a7f9 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -45,3 +45,54 @@ func NewIssue(repo *models.Repository, issue *models.Issue, labelIDs []int64, as return nil } + +// ChangeTitle changes the title of this issue, as the given user. +func ChangeTitle(issue *models.Issue, doer *models.User, title string) (err error) { + oldTitle := issue.Title + issue.Title = title + + if err = issue.ChangeTitle(doer, oldTitle); err != nil { + return + } + + mode, _ := models.AccessLevel(issue.Poster, issue.Repo) + if issue.IsPull { + if err = issue.LoadPullRequest(); err != nil { + return fmt.Errorf("loadPullRequest: %v", err) + } + issue.PullRequest.Issue = issue + err = models.PrepareWebhooks(issue.Repo, models.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 = models.PrepareWebhooks(issue.Repo, models.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 models.HookQueue.Add(issue.RepoID) + } + + return nil +} From 633cd7f473c24ee379e98b9f33ef79c7d664b32e Mon Sep 17 00:00:00 2001 From: spaeps <1037160+spaeps@users.noreply.github.com> Date: Fri, 11 Oct 2019 19:04:59 +0200 Subject: [PATCH 036/154] Add home template italian translation (#8352) It was just missing --- templates/home.tmpl | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/templates/home.tmpl b/templates/home.tmpl index f5eb455b55..91347c7e2c 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -349,6 +349,46 @@

+ {{else if eq .Lang "it-IT"}} +
+
+

+ Facile da installare +

+

+ Semplicemente avvia l'eseguibile per la tua piattaforma. Oppure avvia Gitea con Docker o con Vagrant, oppure ottienilo pacchettizzato. +

+
+
+

+ Multipiattaforma +

+

+ Gitea funziona ovunque Go possa essere compilato: Windows, macOS, Linux, ARM, etc. Scegli ciò che ami! +

+
+
+
+
+

+ Leggero +

+

+ Gitea ha requisiti minimi bassi e può funzionare su un economico Raspberry Pi. Risparmia l'energia della tua macchina! +

+
+
+

+ Open Source +

+

+Ottieni code.gitea.io/gitea! +Partecipa per contribuire +a rendere questo progetto ancora migliore. +Non aver paura di diventare un collaboratore! +

+
+
{{else}}
From 772241b3245eaf4a7930ec48a4634b40699f132e Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Sat, 12 Oct 2019 00:32:52 +0300 Subject: [PATCH 037/154] Latvian translation for home page (#8468) --- templates/home.tmpl | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/templates/home.tmpl b/templates/home.tmpl index 91347c7e2c..d31094a5b2 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -389,6 +389,46 @@ Non aver paura di diventare un collaboratore!

+ {{else if eq .Lang "lv-LV"}} +
+
+

+ Vienkārši instalējams +

+

+ Nepieciešams tikai palaist izpildāmo failu vajadzīgajai platformai. Vai izmantot Docker vai Vagrant, vai izmantot pakotni. +

+
+
+

+ Pieejama dažādām platformām +

+

+ Gitea iespējams uzstādīt jebkur, kam Go var nokompilēt: Windows, macOS, Linux, ARM utt. Izvēlies to, kas tev patīk! +

+
+
+
+
+

+ Viegla +

+

+ Gitea ir miminālas prasības un to var darbināt uz nedārga Raspberry Pi datora. Ietaupi savai ierīcei jaudu! +

+
+
+

+ Arvērtā pirmkoda +

+

+Iegūsti code.gitea.io/gitea! +Pievienojies un palīdzi uzlabot, +lai padarītu šo projektu vēl labāku! +Nekautrējies un līdzdarbojies! +

+
+
{{else}}
From ac3613b791884f29c386bda2ccaad5c7434d822c Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 11 Oct 2019 21:34:17 +0000 Subject: [PATCH 038/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_lv-LV.ini | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index ff930d8da7..21a1b7e269 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -299,6 +299,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 +318,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 +579,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 @@ -676,6 +680,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 +707,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 +839,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 +988,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 +997,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 +1139,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 +1221,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 +1275,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 +1327,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: @@ -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. From 5e759b60cca3cd8484a6235fcc9120d18e8cd455 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 12 Oct 2019 01:13:27 +0100 Subject: [PATCH 039/154] Restore functionality for early gits (#7775) * Change tests to make it possible to run TestGit with 1.7.2 * Make merge run on 1.7.2 * Fix tracking and staging branch name problem * Ensure that git 1.7.2 works on tests * ensure that there is no chance for conflicts * Fix-up missing merge issues * Final rm * Ensure LFS filters run on the tests * Do not sign commits from temp repo * Restore tracking fetch change * Apply suggestions from code review * Update modules/repofiles/temp_repo.go --- .../git_helper_for_declarative_test.go | 31 ++++++++ integrations/git_test.go | 62 +++++++++++---- integrations/lfs_getobject_test.go | 5 ++ models/repo.go | 2 +- modules/git/repo_branch.go | 2 +- modules/git/repo_tree.go | 22 ++++-- modules/process/manager.go | 14 ++++ modules/repofiles/temp_repo.go | 38 ++++++++- modules/repofiles/update.go | 25 +++--- modules/repofiles/upload.go | 11 ++- services/mirror/mirror.go | 2 +- services/pull/merge.go | 78 +++++++++++++------ 12 files changed, 226 insertions(+), 66 deletions(-) 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/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/models/repo.go b/models/repo.go index 50402c8e1a..8db527477b 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1093,7 +1093,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) } diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 9209f4a764..3e1261d294 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -165,7 +165,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_tree.go b/modules/git/repo_tree.go index b31e4330cd..f5262ba81c 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) { @@ -61,6 +64,11 @@ type CommitTreeOpts struct { // 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 +86,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 != "" { 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/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/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go index f791c3cb96..4a50e64192 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,6 +256,11 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t authorSig := author.NewGitSig() committerSig := committer.NewGitSig() + binVersion, err := git.BinVersion() + if err != nil { + return "", fmt.Errorf("Unable to get git version: %v", err) + } + // FIXME: Should we add SSH_ORIGINAL_COMMAND to this // Because this may call hooks we should pass in the environment env := append(os.Environ(), @@ -264,11 +271,21 @@ 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"} + 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 +345,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 +358,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 +383,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..1a1fe6c389 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -313,12 +313,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 +335,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 { 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/services/mirror/mirror.go b/services/mirror/mirror.go index 3339f72329..7bfc5fd4da 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -91,7 +91,7 @@ func AddressNoCredentials(m *models.Mirror) string { func SaveAddress(m *models.Mirror, addr string) error { repoPath := m.Repo.RepoPath() // Remove old origin - _, 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 err } diff --git a/services/pull/merge.go b/services/pull/merge.go index 6150c1518e..e83784f31e 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -11,7 +11,6 @@ import ( "fmt" "io/ioutil" "os" - "path" "path/filepath" "strings" @@ -22,6 +21,8 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + + "github.com/mcuadros/go-version" ) // Merge merges pull request to base repository. @@ -66,20 +67,17 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor headRepoPath := models.RepoPath(pr.HeadUserName, pr.HeadRepo.Name) - if err := git.Clone(baseGitRepo.Path, tmpBasePath, git.CloneRepoOptions{ - Shared: true, - NoCheckout: true, - Branch: pr.BaseBranch, - }); err != nil { - return fmt.Errorf("git clone: %v", err) + if err := git.InitRepository(tmpBasePath, false); err != nil { + return fmt.Errorf("git init: %v", err) } remoteRepoName := "head_repo" + baseBranch := "base" // Add head repo remote. addCacheRepo := func(staging, cache string) error { p := filepath.Join(staging, ".git", "objects", "info", "alternates") - f, err := os.OpenFile(p, os.O_APPEND|os.O_WRONLY, 0600) + f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return err } @@ -91,25 +89,41 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor return nil } - if err := addCacheRepo(tmpBasePath, headRepoPath); err != nil { + if err := addCacheRepo(tmpBasePath, baseGitRepo.Path); err != nil { return fmt.Errorf("addCacheRepo [%s -> %s]: %v", headRepoPath, tmpBasePath, err) } var errbuf strings.Builder + if err := git.NewCommand("remote", "add", "-t", pr.BaseBranch, "-m", pr.BaseBranch, "origin", baseGitRepo.Path).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + return fmt.Errorf("git remote add [%s -> %s]: %s", baseGitRepo.Path, tmpBasePath, errbuf.String()) + } + + if err := git.NewCommand("fetch", "origin", "--no-tags", pr.BaseBranch+":"+baseBranch, pr.BaseBranch+":original_"+baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + return fmt.Errorf("git fetch [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) + } + + if err := git.NewCommand("symbolic-ref", "HEAD", git.BranchPrefix+baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + return fmt.Errorf("git symbolic-ref HEAD base [%s]: %s", tmpBasePath, errbuf.String()) + } + + if err := addCacheRepo(tmpBasePath, headRepoPath); err != nil { + return fmt.Errorf("addCacheRepo [%s -> %s]: %v", headRepoPath, tmpBasePath, err) + } + if err := git.NewCommand("remote", "add", remoteRepoName, headRepoPath).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git remote add [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) } + trackingBranch := "tracking" // Fetch head branch - if err := git.NewCommand("fetch", remoteRepoName, fmt.Sprintf("%s:refs/remotes/%s/%s", pr.HeadBranch, remoteRepoName, pr.HeadBranch)).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, pr.HeadBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git fetch [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) } - trackingBranch := path.Join(remoteRepoName, pr.HeadBranch) - stagingBranch := fmt.Sprintf("%s_%s", remoteRepoName, pr.HeadBranch) + stagingBranch := "staging" // Enable sparse-checkout - sparseCheckoutList, err := getDiffTree(tmpBasePath, pr.BaseBranch, trackingBranch) + sparseCheckoutList, err := getDiffTree(tmpBasePath, baseBranch, trackingBranch) if err != nil { return fmt.Errorf("getDiffTree: %v", err) } @@ -123,21 +137,37 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor return fmt.Errorf("Writing sparse-checkout file to %s: %v", sparseCheckoutListPath, err) } + gitConfigCommand := func() func() *git.Command { + binVersion, err := git.BinVersion() + if err != nil { + log.Fatal("Error retrieving git version: %v", err) + } + + if version.Compare(binVersion, "1.8.0", ">=") { + return func() *git.Command { + return git.NewCommand("config", "--local") + } + } + return func() *git.Command { + return git.NewCommand("config") + } + }() + // Switch off LFS process (set required, clean and smudge here also) - if err := git.NewCommand("config", "--local", "filter.lfs.process", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := gitConfigCommand().AddArguments("filter.lfs.process", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git config [filter.lfs.process -> <> ]: %v", errbuf.String()) } - if err := git.NewCommand("config", "--local", "filter.lfs.required", "false").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := gitConfigCommand().AddArguments("filter.lfs.required", "false").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git config [filter.lfs.required -> ]: %v", errbuf.String()) } - if err := git.NewCommand("config", "--local", "filter.lfs.clean", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := gitConfigCommand().AddArguments("filter.lfs.clean", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git config [filter.lfs.clean -> <> ]: %v", errbuf.String()) } - if err := git.NewCommand("config", "--local", "filter.lfs.smudge", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := gitConfigCommand().AddArguments("filter.lfs.smudge", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git config [filter.lfs.smudge -> <> ]: %v", errbuf.String()) } - if err := git.NewCommand("config", "--local", "core.sparseCheckout", "true").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := gitConfigCommand().AddArguments("core.sparseCheckout", "true").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git config [core.sparsecheckout -> true]: %v", errbuf.String()) } @@ -163,11 +193,11 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor return fmt.Errorf("git checkout: %s", errbuf.String()) } // Rebase before merging - if err := git.NewCommand("rebase", "-q", pr.BaseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := git.NewCommand("rebase", "-q", baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git rebase [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) } // Checkout base branch again - if err := git.NewCommand("checkout", pr.BaseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := git.NewCommand("checkout", baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git checkout: %s", errbuf.String()) } // Merge fast forward @@ -180,11 +210,11 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor return fmt.Errorf("git checkout: %s", errbuf.String()) } // Rebase before merging - if err := git.NewCommand("rebase", "-q", pr.BaseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := git.NewCommand("rebase", "-q", baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git rebase [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) } // Checkout base branch again - if err := git.NewCommand("checkout", pr.BaseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { + if err := git.NewCommand("checkout", baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git checkout: %s", errbuf.String()) } // Prepare merge with commit @@ -216,7 +246,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor if err != nil { return fmt.Errorf("Failed to get full commit id for HEAD: %v", err) } - mergeBaseSHA, err := git.GetFullCommitID(tmpBasePath, "origin/"+pr.BaseBranch) + mergeBaseSHA, err := git.GetFullCommitID(tmpBasePath, "original_"+baseBranch) if err != nil { return fmt.Errorf("Failed to get full commit id for origin/%s: %v", pr.BaseBranch, err) } @@ -249,7 +279,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor ) // Push back to upstream. - if err := git.NewCommand("push", "origin", pr.BaseBranch).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { + if err := git.NewCommand("push", "origin", baseBranch+":"+pr.BaseBranch).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { return fmt.Errorf("git push: %s", errbuf.String()) } From f1fdd782d57ddbd2a759dad2843ee619709a4609 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Sat, 12 Oct 2019 01:55:07 -0300 Subject: [PATCH 040/154] Add check for empty set when dropping indexes during migration (#8471) * Add check for empty set when dropping indexes during migration --- models/migrations/migrations.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1f5b918de8..e14437a04b 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -404,9 +404,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 + } } } From 0a96e59884ca5c4fedc8c3d166d97f35b245ad6e Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 12 Oct 2019 16:45:00 +0100 Subject: [PATCH 041/154] Fix #8453 by making openssh listen on SSH_LISTEN_PORT not SSH_PORT (#8477) --- docker/root/etc/s6/openssh/setup | 1 + docker/root/etc/templates/sshd_config | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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 From f2a3abc683ad4b2177b7c7c6160a2c0b4316120a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 13 Oct 2019 21:23:14 +0800 Subject: [PATCH 042/154] Move migrating repository from frontend to backend (#6200) * move migrating to backend * add loading image when migrating and fix tests * fix format * fix lint * add redis task queue support and improve docs * add redis vendor * fix vet * add database migrations and fix app.ini sample * add comments for task section on app.ini.sample * Update models/migrations/v84.go Co-Authored-By: lunny * Update models/repo.go Co-Authored-By: lunny * move migrating to backend * add loading image when migrating and fix tests * fix fmt * add redis task queue support and improve docs * fix fixtures * fix fixtures * fix duplicate function on index.js * fix tests * rename repository statuses * check if repository is being create when SSH request * fix lint * fix template * some improvements * fix template * unified migrate options * fix lint * fix loading page * refactor * When gitea restart, don't restart the running tasks because we may have servel gitea instances, that may break the migration * fix js * Update models/repo.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * fix tests * rename ErrTaskIsNotExist to ErrTaskDoesNotExist * delete release after add one on tests to make it run happy * fix tests * fix tests * improve codes * fix lint * fix lint * fix migrations --- .gitignore | 1 + custom/conf/app.ini.sample | 9 + .../doc/advanced/config-cheat-sheet.en-us.md | 7 + .../doc/advanced/config-cheat-sheet.zh-cn.md | 7 + models/fixtures/repository.yml | 43 +++- models/migrations/migrations.go | 2 + models/migrations/v99.go | 34 +++ models/models.go | 1 + models/repo.go | 85 ++++--- models/task.go | 240 ++++++++++++++++++ modules/context/repo.go | 35 +-- modules/migrations/base/options.go | 21 +- modules/migrations/gitea.go | 36 ++- modules/migrations/gitea_test.go | 7 +- modules/migrations/github.go | 4 +- modules/migrations/migrate.go | 12 +- modules/setting/setting.go | 1 + modules/setting/task.go | 25 ++ modules/structs/repo.go | 16 +- modules/structs/task.go | 34 +++ modules/task/migrate.go | 120 +++++++++ modules/task/queue.go | 14 + modules/task/queue_channel.go | 48 ++++ modules/task/queue_redis.go | 130 ++++++++++ modules/task/task.go | 66 +++++ options/locale/locale_en-US.ini | 2 + public/img/loading.png | Bin 0 -> 18713 bytes public/js/index.js | 36 +++ routers/api/v1/repo/repo.go | 4 +- routers/init.go | 4 + routers/private/serv.go | 9 + routers/repo/repo.go | 103 +++++--- routers/repo/view.go | 30 +++ routers/routes/routes.go | 2 + services/mirror/mirror_test.go | 29 ++- templates/repo/header.tmpl | 166 ++++++------ templates/repo/migrating.tmpl | 31 +++ 37 files changed, 1192 insertions(+), 222 deletions(-) create mode 100644 models/migrations/v99.go create mode 100644 models/task.go create mode 100644 modules/setting/task.go create mode 100644 modules/structs/task.go create mode 100644 modules/task/migrate.go create mode 100644 modules/task/queue.go create mode 100644 modules/task/queue_channel.go create mode 100644 modules/task/queue_redis.go create mode 100644 modules/task/task.go create mode 100644 public/img/loading.png create mode 100644 templates/repo/migrating.tmpl 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/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 9bfddc97e8..dd14089d2b 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -808,3 +808,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 connction 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" \ No newline at end of file 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..ed34be032b 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -514,9 +514,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..01ba821a47 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -241,9 +241,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/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/migrations/migrations.go b/models/migrations/migrations.go index e14437a04b..ef5cd377a6 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -252,6 +252,8 @@ var migrations = []Migration{ NewMigration("add repo_admin_change_team_access to user", addRepoAdminChangeTeamAccessColumnForUser), // 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), } // Migrate database to current version diff --git a/models/migrations/v99.go b/models/migrations/v99.go new file mode 100644 index 0000000000..3eb287af6c --- /dev/null +++ b/models/migrations/v99.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 ( + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/go-xorm/xorm" +) + +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"` + } + + type Repository struct { + Status int `xorm:"NOT NULL DEFAULT 0"` + } + + return x.Sync2(new(Task), new(Repository)) +} diff --git a/models/models.go b/models/models.go index e802a35a77..ea550cb839 100644 --- a/models/models.go +++ b/models/models.go @@ -112,6 +112,7 @@ func init() { new(OAuth2Application), new(OAuth2AuthorizationCode), new(OAuth2Grant), + new(Task), ) gonicNames := []string{"SSL", "UID"} diff --git a/models/repo.go b/models/repo.go index 8db527477b..23b1c2ef52 100644 --- a/models/repo.go +++ b/models/repo.go @@ -126,6 +126,15 @@ 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"` @@ -156,9 +165,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 +206,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. @@ -884,18 +903,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 @@ -915,20 +922,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() @@ -942,11 +957,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, @@ -955,8 +971,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) @@ -986,7 +1002,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 { @@ -1005,7 +1021,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, @@ -1143,6 +1159,7 @@ type CreateRepoOptions struct { IsPrivate bool IsMirror bool AutoInit bool + Status RepositoryStatus } func getRepoInitFile(tp, name string) ([]byte, error) { @@ -1410,6 +1427,7 @@ func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err IsPrivate: opts.IsPrivate, IsFsckEnabled: !opts.IsMirror, CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, + Status: opts.Status, } sess := x.NewSession() @@ -1856,6 +1874,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) } 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/modules/context/repo.go b/modules/context/repo.go index 3caf583f83..f4af19a0e8 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -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 @@ -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, @@ -425,12 +415,25 @@ func RepoAssignment() macaron.Handler { } // repo is empty and display enable - if ctx.Repo.Repository.IsEmpty { + if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBeingCreated() { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch return } - ctx.Data["TagName"] = ctx.Repo.TagName + 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 + + 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 +442,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/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..ab3b0b9f69 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" @@ -90,16 +91,33 @@ 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, + 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 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..1c5d96c03d 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -34,7 +34,7 @@ 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 } @@ -44,7 +44,7 @@ func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error // 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 } diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 27782cb940..3f5c0d1118 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -6,6 +6,8 @@ package migrations import ( + "fmt" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" @@ -27,7 +29,7 @@ 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) ) for _, factory := range factories { @@ -50,14 +52,18 @@ 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) + downloader = NewPlainGitDownloader(ownerName, opts.RepoName, opts.CloneAddr) + log.Trace("Will migrate from git: %s", opts.CloneAddr) } 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/setting/setting.go b/modules/setting/setting.go index 5e476854b2..8c61bdbb77 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -1043,4 +1043,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/structs/repo.go b/modules/structs/repo.go index 87396d6ce9..57f1768a0b 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -162,8 +162,16 @@ 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"` + 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/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ca09b6120d..e6c5839a64 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -633,6 +633,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 diff --git a/public/img/loading.png b/public/img/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..aac702cfd6d010abb22f4ebd8f011b606075ef62 GIT binary patch literal 18713 zcmZsCWl$VJ*EQ}A!3i2%7I$}8+%@RpE(sRgHRuA1ySoN=Sr)foA-H=81j3i+mHPg? zQ#I3f?%cUEbGo~x&Z*n6n(B(!7~~jmaB$elN^;t8aPYu?HVh5%-_fdn`ac)YEUk6_ z<3#$m!v)TO=KeKnnm}Fof30#Ri$|@lU4^nunVfbmFA&V3kiqCssp?v-VUWjXTp(mx zC<3YgT9?XdWOF)JYnv8|s)5-J3ng^&gp3P>&5Gr$%YmQ@4V6q5%TguPELOQRhPSu3 ze`(L}Pt0>j$F_U_o$@D&_9~oA_2E1C{O{XnytUPJ;Eu4FH2PP6Ns||Z+qv^M8h@T_ zoJ%c=4^GI;?QS=%C#%1l{ufIQ`frT>ng0#oKjXhQ`2XNmphfTxHQY>PIceR`-_O5q zB;^9|LM$n~_?SXlNwnHoP80OoY}#<=9)lacN7|qnVcBy5-u3^oDl@J9wJ1bLl7@tz7BN2m;Gg05wcOzYk8k^m7E$v7S~6a!Ogu%03&pZqBnwd9!uri9l!MTL zlsM?!1|)Pp23Mf2O&hJU2{(Uudj-gu?8Muh)x)w-T^%ujQ|bhjvsH;_>^D6}Pq2dd zr82CcS_FT0&$S`jqDm|}jl#R^uaf@J3`(Rr0BVi!WIzP;YTwJZ*-o3b#eJbHinbA(uqK*XDgJ;cfw zWVM|udK4Owkt?4E_Ew_*rEDZ7{PSiYB{GyHEkKd(Q3`I;kY3a!zB_J~$&Y@b4O-J`+Ip4Xz zl!ef&zqNd&2J>aphM1R+R1%$cg4}LVjBU@8Q7a z8^Ht{bdK8;BLo%tb?uE2VgRn!5QI}6gN!mUT8eO4;Wb)ae&1UC2Ttstbf=J14x&78 z$v82AJ28cEyRMg265!}l7YdPcmDQuqC4M6>^TByfugTI(7VB1I-w;{=$TjSkj@n<_c5-Me2q zT?r2X2h{wUgoq#r7nIx9u^*}M_!x}&aO+q>Uh^KwIueXuyX4JaGKJu2w(2cuANE-v)~ zgQ?9)ux&h*`Yjw|=ZVH(4_P51t)5AG?N3A|#z<7aI02A$+VbnRj4}6^h7f*MHV2fz zlz9NyKK1}YmGM{ZUpT8CjFo%%P7GRd|2$5^o@@ND9lDnFM~EU!N!Mjz38Or`|AN&J z<1yP<{2nWVj>MjmKOU zgZkjX?OP^yu$bZkY1;e6C5(#jfFuK7(Ed{QrN=^P*%bJq!EO0k=x&R)&qt=%{aki_ ziK1m0WC7VU7wYt}neQR7_~igq{7x;miuiN?I{qb*6}4ZCIY%jb0SprIV|YVU|Gxds zD*BPO8kd`KuhyYZIo5=qDLS&7oYclVW2{!vqQ3u!7@Ydy$dQi7(O83HGdHi5bCU=~GNw>+dS_jE+ni|80$d-t&#cLkRd=qZ5X8y|Pm}v&t^peC6 zVgH7T(Ya?xKzO3Iz+p~AGP-|0FN~UMy=@eh9btZ8WjJqc+cq#s|E+rsUJhmW>rxqS z>Q#mfgiCQRJ;3{A=8ZoNJBohr-#=RMm1X(tgxb$wF;@Ujj{D<>xe)EDXmi#@v*a-} z=a0_lIC#i^-`rUFTqM8H2a{D0$_ZtY_8p^nE2M)$Sh?Vg7mJ^KI;N z2pa=*H8Cn4o12tLDw_zUKJVL6*a@zOZ`-S|)+HU(%UT_S(NZTFD8~LlgJ+-usaWFr zsC!5(1~A+7wj~A>TV){R`als=6t2iA@gR-cR1Q$ZKRoN-aM{9o z_P<-`K^&@OLz`92EGYglBW=O(8BaF)3SoVBI8TNO$EmvX8E0)dYEn4|-Jyq)gEX!h zkasv-yVGkJ&0F;h!Em#191fVlLi_akI5L-)uqC&*8mRvIB0Nus$LP!lMSIasuQLr( zJKMJQ`nmXpG!Wt&niow1f@kf&#s01QK13YZT38_OiBEuBqg>owlEgkFj*=Xk&G-18 zd9naX#`=fPhR)2`E37SiJ*c0FYP}m#cgJ)BX=tg%Gd7w0j8*O6b;MpM|DN8uEn-p% zydaK9%4Ia0g{d$EqL<0iXKrI$&e6foDMb7ZAA9!X`2yxsU`^-DK&?EzmP3*)f$mIg zeMi59&jo8c)!xFrlfa23o*GJ^L}e1YJG(dL$wRGlhY#n`vmEneiCX8YDnKv&_XeGE z`KKvntI&QK_OB<_n!YdRR&5ZMtVYxYd$AgH5Aoq9>VruGtVNIf z;4C@OjQs{J1!BLIkoa{LE#>I?gkY>D5YXJD2s)5%WseU!Bx18d<$v-?eGrt(4g(A( zaQdF=zs`*eL2D8=Q7TLE`jLlpnMvUjifcw$-e4x+0IPq~JA~C*7sO>)#)eWN+V1Ij zimccc)n%KSZ1r^a-OxJ`X8yLdYGA>a4m25rdEoBmvk+?@FaXu1l$AeBj2x@i&Q+06 z;)@T$^m|BYLv7074|@nqjeEv zK0Vx2!9w#6I@qE|d2HDxG@@#jQ-=h>o8nk?kbLP^VK>TvGQVUMO0v$mIbX?3(nR_{ zwX=Mcc{=)Oj^Ssjc9xeye;+T8kh@=vY-U=dbTmZA8S2)B!=s0wvBtY!U{7KvgaqHA zFkE$Mfh(;G7jq_PW@mwyM8#wH$B6G`nsKM@m%f}>KfvO_oBG_7sBuF`m*cX9xGfXL z^y%+C9zJ}K{HexmdKhffI?R6I(XFG~zA-P;@#w)5BVl+)_$%gsx2|ew<2C9bm~d47 zZx%tGoZ;yZ#3$Wj^-UxnJD7h0t!XUkc-Xf<~`_=)Ge zf|gA(ppy9XeKOatjnfpNB_x^Lj@D`bf_59Ws5pRl*PXt+s$IwcN zNfOI;an2#qcs`1`u6H678VVjo9dZclU*^Z|bBH;Ae$EX@sRC3W_yU{|Bo{+YYB!Ie zq;fUVk;2FJhz$ywrC#Hl4=})xF6P?9V=uuXP6L@5_&RbUU$(c~J>Um6=B2mF(y{NS z>5P29Ol1k$1IIs-ka{rE6f(UmZ`n5qBv8@`hea56CU~WIJfpSjJUxaijCeD)^$w9q zO7cvb)^gfG+{Dx5rX>NJ9X%KEi-I29{gmRp^pXa9mpmsWN{qpq#<0*%kp^X%tdX3nD})Ji!xW5 zD}IHt^6E$ZqRPN;=1p6zpLuE1qGlZOxTM{|!shwuiLwK&3xAHOa%JrF#Tnz}n;V)n zaea-gzi0djLC&K+u_>n)+RQb-m=2P9WoAB(G5j%OyVSjT6xh%dyMyBVjnysKjOB@V z4y5syN7+qs!8c>oDeZn`xK+SZGJb0ZF1Ni*`04Qy#(`>y7gIb+^asx6zTxH$hO7M8 zFQJp8l(4cQH=UQ?7?uVPLP6kyNW1a=9jHK@?L$Z2?@#C=A9qQU91bilYe#RVZ$%cd z%5@YdK+Rk!hpUhws}+!!Jt!F|Z|sO8T3Ric5`Cs^JbNX*@n76n`O6tL+Vi3>p zeC6U}&%k5QULH1=EHBnh2jc9Rph$_9AYZ|04(P^KnI<0GhWcTH!P+Kjb{uW%ErBlK z#-`nbx;*~X^IJBiY3D#jf4Hvo@Sk<8Vb@uwFSdJ58&O3O4X&i^W;Mtk`~6?d1+ivk z3pSt};yQ@_WY=haIPbOA?|%@&XL_fDDb&eqM{AEGMBjBPnhAF|V}T9H=@02QFR)M& z72?%AEdHR(dhDCuj76RZMa6Jw%sTOthYwmz%>AxyVjoJgS;gc~GpKnAY+2{b`ZH=J zB3`@tZQZ%q1R>IVvWU_?d;d-k-Aw+s-Z{llyI2&P!efHqU8G6hcno+S>zRAC zZsY3N>(m%VKe@Qsi^|H#vd*K7aVQLiE4pIkwN|DRG|{Fs zyX92O`O8^`+1OKiJfQt@hQKRlj*x7?#(SQhx=D&p3s**5t#TC{ieU8Cc*|TR0ewp4 zj`MLpGckC_Ei(yqMNQAi4KQ3wAk=Ety6ISsJ?is#23d>yVGA^;MZYIqQBLvqGX5PC zd?*8?lc>YWP{yg>!W;`2V{7yKHkg(WWE+sJEh0DSrw?C-_TyRzbc6Jfe)_s8);_Z> zpOQP+ouP=K4CAj_Lh?s9_8c8;p`5nvQfT~~e8QXmf^pbK-=T#1(!a_#IbqSBi)PA3 z5nYKM56VDOt@${Kq8?{#I3ZgP^gPARTgGMUu>h3>%4uSFnuS_8-qB^Jd~0?05AvcD zCPzh-(?7?gK+D#wjZkj$dAzL&hb&T6KL}f+18WGGOI3ME4c5hicH*AtjcwB2Xr0<( zBV6jyEqLQJs|x`YFKGpCkb>P$E_Tlp8HyfC+zl1(2oxOeLI9IiG+Z(J)YX>th&KEc zGO(Q8|CG+yJ&~SykbHN_-Gq=v=3-BkcQobI<6Jo8Q9s0!+@ULLx9n=v7yZra1K@E< zh9??MOfspf>PvP_qunj08iP|-cXZDeuh6&BWZ%5lk`v+b$F*JF_e^34jWvOM8N=Ra zDX9sQPXR146&Y?hDi-fyfML&gF=1{H^$*P-LIzf|`;TeN(bcJzNuJy$eM`#evt3!PD`mN%QD?P@l6mr~Nh-a#PTfM<}mnUX=W z^_OGuyX_=pIHhsv4$qok*V46o2LzTcd`RWVuB(VVPlQn1PAK|MGNja@59-FuTpFP@ zz8?U%WFXYSIqeoLSAn^3ndzQlah+}W6XF!Q>F)5`J9IPV2qP_m78K2U>5lFs4A0in zhVOa=_=>hH~EgEHDd7Bb9vo-qshPE zAEBRF0A@5#6d;PN>5`0(_SOITaTJrEgM=5}sxSZ zqEN^;wnse9K>`jf@>TZbZIwhful@7qdd1)YHR<6Z^d6(vWESnbeUjwO+g`5uzD=0SrVGBWM`6Dg_p8e zO!fhMs249Z*Zjk*36%uY&Q(C_=pJWM@=ZbH(IUI$mU0e%K<*B=Pc-;>*0MUs%q;{u z+2iQ>yI=IN5N2b<_nto-{o7r@M+=eEFOFa0hA82<`U^}y!|8~tpXFL^Ux>Zj^sZmN zFF$Q@GEL8M^wkqIa@74K`*dhkK8I@XJ#@VSbw+Ao7+hsCe8GMv?cIsRAo!--kSV+v z{i+H6gIT1=N?*o`NMT+9Mb=0D__Je&eM zO59WRm>)eDVG#5^N<^aa{DW(ITn72h3OQY&^`#4*{vA`+P`>Y6?0_Ne&?L*Lj4;~~ zmUrrQfZf1*QLoiw)mdY^37Y_eJ|sv4H{A!FPP64~$?<~-_|_lSUl{9mJ2yoA7%=fU zwCTWKzS5|VG{+>Fhx)nT-nE`Jiw{~HdH`01%#We4~CMlfp+E^V66ri8{kqPRKJyEN5s;u25yqHu(Gg>4Sa^w%vo~MyL2|RFNNVmv(Vu4dSDt`f{2NH{ zTDi5Y{2&(y7F`kEJL!X=gPu9GPr=xD5-+!*3eqhO^dr09B$L}~P-?K*(AO-oIGvn< z1Inc*LoRqMPb`?HK3GWDXfuHRht|f)-7EgMlHMXCLVb7VSl_C(`hw;-&Q7EVMQCf- z6J8h`)0*pJbI^f=yj#~G>I+^(49(lXvvVITI*J?<6|O(eu!iSB< zeG90W+N5(;r-S=lv68B9*X}C?nsI*$jN^mWPX!Fb(`EjF2WzrZK!QHotQKS#b-;3QCszG3;Xxj%>7$A2w?RfXPOtamH_b5o;4z=!rasl(~W+UH5!u9Nx6LF5;1=N|>>s*&h?l-kOM!Z_P zkw)c7MTW_wCviscJxONg{)MI|$A3=tC-~0xB)i>qQI3tIl}kGH0bQ*|Os<->kpSmva=>-6ME{f%};g)A?A~m)J*(s4EEsxB>P+=5z3Vk zDP)A%jBAJQN0m7j?UjpoVvp~sJ^@bl2qdr7$pwbVrN?YjgSyYfi^T1dezS)7`TOI*GMaH1^a;TR1Tu3hNNhO<6 z-2GESxqaR#0Jk~WpyGd9uG-n~w$~v_tZCF=eUx<{e(LTNi-;N&Z~%t_6Q!svgUXex zc=9(@Fhiuk;8rCac+b7>V?+qfL>i|Pp_C>3}UI3JpTBLpq2%&_y} z%hZn=JyitSZaQ#omnFAd@P;&s)~jH zz8y;7w!{KUo@j+MXpvtDxZL}N{N?V8H`g&F5gjk90mK5w9;b|iyVR@}A+w)keAXx= zhC6W0_Llv7iS{>uk8AnS+3d0b_Pwl56R3l&Z3 z6UwPpqsuQ!c^rblhDyK|}2sDE;&@ppx?tU?0 zxqFr>kKg^bxa0IDFH=uzE^WXOE_WIME3gz<~Sl z9J+78qT>sV31i|+q7$9QI(=!Ai^4C(F&eYJNlrRE29R@idf)5B{5;R`A4L_o+q@5i zvwj8HfqZvV^uY9YjFHsTw^$#eK@O+OFk0)RhP%D#xOa!-dw0e+@ngAR%^E=&o#@Xn)J&EzO90n8rB|Yd>d@>?Xa89rVYOY$QWq|& z5jLVJtZ%J$iu4#&E>akSVeUkdl0BbUZSnBcck?>es?iV<$EKGMbAHoFV9m8#L8w&3 z+ezg!%w^mwEotpr$Y1vP()Vlivl`K1Mdv2se4-P#jkSGQ5GCWL zO~`DBRd6>IRzgn@M%uOp{@9Q5;KzHu^GM6ef}+lBh@XVSR^3#P)m%ZuXSOdl_lq8j z$$fuco@@-X5th@(z$6nN-WY({>&c>WnZ9tGB$+m@DaxnHV3(PApog6(#?WWC4s=i~ zsL$#^8H_u)KG*dA34xf6LWI{MF0f(9=9bPf;Ud`=Zj9f%nZc5?Lr$j3(p_P5@%Ka= z%+r!Q*pMgIt1F=5wh(XstNJ#SP1^Dct{hDVYU=T(!1}LGmF_Y z?gn97S5skBj26HFt1sH%EMsM!g?n{7 z1SQikOleDn!KXo$vDQuf z3)%DOM|Tlb-|3-t5kNNe1Q=yQ*>7RT(UUdmys?@Q!h{Xqo(aVFy(2V^<5$QLMt}Mg zYowws<6&K)fQF($PK#q`qmnMzAFmZF*;MXPl3fLlGl*f`-8OZ=yQK5CQJ2Fhr-q%6 z&(pKVAEj{uZ`T7@dwI331OD=H!p6M_do^A?-*ZnfA&=|gP9w8N{p3kredf<7tS!mB z$(yw1n}>G?&@lA|Z_vTv0mq$M_LG03PC>9PtXqu?j%J8A1?(+&J6AYKbuvf zUk)DEfUEw^Gv`VF)~kI>Fn6f*#k918x=Q42oenWDz$3RV+R>%^ufgRPc<0m1mHqy1 z(eRasP?`jg?78EDKKAFciy7O&x3@oWYpl!FT3AmK)WS$IoXImV7>q}FkMN0xp`TTx zSsPb|k?=6Tt$hZes@~bY%!STC<{M0Vtz7L|7?9RRcCD3NiX9Wgqo=wKZ0x_#MFiCm1x6zjDlxkL-KvfZ>q|HdGNn zCgoZuz!lweyx!tK)_z#3e+^T$FlA^1YA9rSJBk^2fHjP?^@+-^kv$NsMu zY`S2VoNj-*od|Ev<{BM^|J3XJuXy#({=bS)PFmS6{)tyb|07-@{S&Y9Fh4B-NTW6o z4q$5*0|S_}lRU=m+H*w!t~;!^H%Xo%D(aSU2fa+Jpkz@y(eBXXt51_ze8xrxd@9R> zg%%|75kxK5=BMtwo`O{R^6;iV5X0q!o7j+1pi(jO~p(M-&cq-8u#j8mH{Yno$IxWBVf zKj6W?CEIO3V(NgW@Grgo3)4a8&~@!WE z(j6arK6?y}a%`R4|Jicw7}q0N6aM`?t<(^(?Aoz&KMU7l6(vUHK06gEyOni-U(Lganl_2Hl1LBqPOR*gz7fQMpz2~6>R6B>J zAc7|TmxX4060tDY1sGN(?E$VAnUGDjTkf$Ar6g3KiW-EnB`PCL%DkkfI!U?NpaCHZ zV8YPmg~p$bP0MZrVuuhTZCz?DH`w$_1E0nBBZa5~s7x{UIO(3u{*(|9__7n~7h(B+ z2#PPJjOF3!IH@aG=p=}6_=t8W%(%yp={uQPT^g)0lK>&B3xUuvDzk4je=v{@UUij& z-GdV3g0vQ-1y`F0blV?rcN0R$)DqPR5@J2H4pr^>-i9{^y$Pmp2TLtK)D#AVFT_At zqJ}56Bti-Kw0#Uinomi+K%FjHgL3(RS@w)mU_glj0`NJbD^_(9ol*D$lzi;$4KR9X z6R^>gTsBr~sFKl|9Gy6kz1t`JX~obH+Du-qV?*I(ak?1i(;n}%o-ZWPq&=}BG0z9b zC=Y*Typm1C$hgb1@Oyg#3H0I&aYZ3490jhmlkEgsU?$5&qLGm^0; zvQ=+FLfE4HV9Mgro~}brP%U2{qfnXqY zI|ZEAFcK%-#kva<2h@D!n@AF;oxc_A?X;;g`;!ae z3Jo8=Z-B$b)TrT$d%#_^T23D`w`cD+@-KKo{K`aLkgTDE?qBzSyy5nXFU8#bX4qW9 z>KynuSy_Y12J*rNH9>QAn33FE(XvT*+K6g~*`i@GdMI8o9vrd@=~V1uJ>31K*pcdy00#-N3;A1YC$EV(|E+h8|3a7^%*3Ph#{^s^`cdghQf2ffIQ_gEmsm#V=$ z_YHm7fppmS8KoSDM6%sdjReBWP(suWsCv%im>e!yS}=~#{34nwbHdNXOnsU#jfDRM zRv|MjQ6DEVcwClh-z|+fuMeeQ5O>y^ZWU83tl&A^Ks(HuS3*@6QX{Qq z3C4=CaQ&G0&C$_%A?3i<9x4PI&ho94)P%kD`PIZpgVMW++@?Ln&Q(6 zbL1B9zk|1r1c#?p0p$Xa?Sx%122rJ)I3~{Y@g~7-9kM&|0s{0*N9&=>G0T09T}kv% z{*;lNiYkZh;9z;>vPdQ*^fFUS^|d!GhM9At7F@O|M2fq;Wjt@;9*UDe^k`wJipf+&5BfR6^sD5@OxdK@7yGSwy z#~5dSE7Wwr*0U+8g|y9)uA1;cZwAJx3wenGrl=b!zKS`FdKpq5XCHd|{)&pLd*~w2 z_o6NugvWOC@@?80B;CQwDoEOV6Hh<~rx*rRm0Q}M#Io*t{oE!rzDn>-no?qTw2UK@ z#8AV^)}-(WP%iw0vn`B~B`QF*nMUNm4-w;&v>~|goztxmFbnrEpQ0rm>Sv;5>t0mI zs0ySw$=*`Na{=1JJ6$Gwk&)N)(6ZM!_}!(G#ow8&e`XB~f)Fzv3IRn5?SG~Jd?68Z z0E!@^f8|C}|2l>`Exk4kzl)<52C7k`eg}AWBzh+IF(e=O+!E{odsm~D)(bp1MdLGT z_J71X(igz6#?TE(r<-cJ3-(Zu{g9*oUTFk`lnQb$ZzLGOwFd51Oy=z9iB!RdXuN?;h$XnFQLQ0}?5eT}%=q1a%0ntGXF%)r(8w zvXK*#bChn*5j9-^j?ylMDi`P-2i93f*X5~U)!cR;Sf1;#xk!kBgnz82rgJfoL|!@P zCgpQuyyC~<18JvYl0n+cMqW0B)~#p2zvu{N@JB8AtDie@KMmKBc_CX9+|hTniz5Df zg`=Y9nLDwtb43+Xd)ScuV11bru82TZZJJpwIp>9v!v}-d{rHvtiNfUd7n|!0SO`bT zCu8I@)3i`4(SauFa5>#9g8fl#JpR4-&&`FH6IJy`X*ru^u$Wx>8FubK*Y0GPTitfo z>gM^xU+&f*b$JpHCHcg`d-xWr5RhIWd$4%Z-Jqy6#6$#V;L{L9Ij1vLzkH)`)k8&p zUa#O6``@?8WLqFZVdOHtS1L6o%|loyNUxZpRLIZr3Zl}LDU%|K-4z;V<{&yK{B^;h zba0hz8nyB`foo(gi5veQDGv7Q7h{q^^vl3lgto|=JE1{rzZZ^-SK}4(`u8z8COJ-L za|kUuopUY%(usag!c+i;g1oi9O)7?*HfQVc-EG(RL?Jx3L*0G zCsG!YdzK;U+K(vuCd5X~V^-wA*0v9OKmVo z=drGjVAmW1d-v;%5DOCCq7Un8n z)sDU)i{=n7&qo@euVd#Me23R0S~dht+g8Fb={BJT-xtW03Udk`w^97mgd;01V(~=j zew*hf6vUP%h`A!gU#f$BEc+Nsf+kDxRQ!55*6dXFOpD$aUMp0N|*5jby3c54@Xk8 zur3rJu&pSs2LFUMVe*fXB+AmYL^u35{^NlkFM+xWN;jon0^zK!XlEE^VNrG{tq|tk zSa3eP2MbY&7_Af3(lG13Wu_%yz8iTe41r}nda>&JNsf45K2u+#(9_`P>d3j`a3P5} zi3u)GY4)$nj%e;6orFMlZmaBPZ58L_78@Y+i*wa{V7}44kvfqk7Z++|W0&St<6kOf zVH9_Jjt~=0b~3`5j*cYTw%*x14X%D$C9i6@cr`_lGl$i$DcK|5ukNCTc(SYs4sMaF zVhf{+)oZIsZeC_vbBCXL@i=%7**N5B{M3vV1GApeHJ7rk3Y(B-JPg1$pueR?J*^T|RD7+btUXQk& zf}`gjgLyO-JopBvP!u)k+4$on9T`Q1=!<-!aos$*h>0*=P7jm3mLLOWf3d7~SI1ls5vyPD% z<3!pgv2dG&xp}sKhWm<_6=vu@4D?LhW&8aOuA>j3E~M8xiTT-_JS6fZ%!Loemz&mQ z-=o*cgj;7PN7ln45kq4L0wyYyi#t`PWoVa<)G`wn8ZGD>PddI#?I?~px8 zDmC6)dyb*&@s}S_erznKf($FEG&#&C#*iJ+3zqNo%&om|@a0%{guEq5UP_vZi_=;Y zw;c|2#Pc3@biGwk@b>4UhSrA?jj>J2g`)mM{&mw zNZshMSVzb1W9LE-1PEI~>VL+Zh4>dKY`k@ZUY}x|eNWLEe3sQac_tZPV39u+Zot*5 z0?lrU(d_h}zyi}=^-VrId^@@zdg*O1$Ex>HYAr$})6sz}P@95w9aVl)tXz;soD0M> zKt1VGw_`OU{_*q0XVZ<#T)dj)(#^k(=nyT>ysK%$5BR?b&QOHUL>}I89`n^(ar36} zNfR`!_!qcfVDY=4*f~=`lFu_GOD8-jOgKL8zJ8S$8TNf;h~}+i=k#Qk+)w5c zsne+<9QK;UkD{ynA&y@2S6AQS?v0!I?EKr0dm@mVcFS`ZE7c=N_vZLb((N|mV-e-| zVk%(KkwtkY<4zog!Iw>`x&KsH{ja2j{C`VYNMDSy{z+OA|08Lk{C`PnQKo2<3}7K@ zUa34`FHI>cJ>ElA@%BcY4R-z5RPIp0jvtitK2N=3cAM*?t8FE7{gt2y)ohhMdX-2L z@3Hr3MT!3E<3?F({NFUfj-}d1N||e_2!QlUREo*TL(gCLO=p2`w*C9viVmwjTgFuv zS>F!I3nMUw<+uG9AWwl3x%fTX+qDe_Y&R6_(l6@EtQz&J4FxyHx99DCzD~gz=A3o9 zjvcSAHT^Y@;7Q*&e`AW zq1p_F9x=Z;_1~x>qM}C&C}K`{0<=eVmtl?FedY~r5<%Zx`&|Wcr`^77T`cQ)e;dV0 z_Ka%Y9bsf*IdoPFC*o7+&X|1X+aJ0u+RrjfN&{J*!q@c|W6}@_`w}c9CP|&Bu;-yV zcaUzPVV&Qh{@X1NH(Jb?3jWR7T;sc^J{t?>oc*`2-#NPkEIG(1;1iz$w~`OOsL7ADkdAWCaU-lr4zQ6D(lo3@ukX{wsu5fXQ6qu^x{3#73nBM*!aw9pZ|fSN{Ft+*e1bv4W|$k0j0m{%XY+D* zhOWP~D%}LlrJVS@M)`&8dHm9vA=jz9Ug>Ixx~v1gO}Mjj&=89vE(u5EBs=5Q>cy6tonkak(#5y# zlx~(jz}>%WshAxOZ59B_f$tYO(oqhN6xt`3kiB4f`e!cbA#7c zhm5#EV{A05gACKQWl>A5=o^L%i=^JeN@83&wh38D@jR8nV|Ew%>3*cs=Qc=$dyd$) z+T6iA6Li_GdRt%67+um6?f!-=L~-k!x7ryzQM%4CpHLLof7d!R1^IG~cSd`+f^TBW zv3d6XaG#n>jF(1u^Koy#X0fO-eQzIqO_)1GC9EyTRNUl^Ei|h`a@(z{II^mJz^VXWQ|$8+)1jBBL%ep!t7WrUSE8F=<9 znjUl7)EWCNo2Q}feFLKW4X%DhSKu0#>LG^BDJkV=Y$Qg9PE+?xT5P(2R2Dmh99LHZ z)VMJBo1gi}s}9p(7FQRaub_qetQG6pP&)D&;F&2{l~Js_fTJZbMc%29Iq<6${+P(g zP&#mp7a`jL;6N+YkNL?Wd*03+yfE3nSjL@w9oTwXj8BW5PH`0?xIvr6ng@S?OpTt$ zI0jsYp54xebool^XzW=m`hs68{oYv7sy_dl_#w`R)``&fQ)*9hi@)SEp{pcm9WoVU z`wlr=w$-yCny#h~t*_UEFQ2)yJ@XV;DRYFy?2BBF1Srkq)&~RqxU=2!ba(|0rZ&4M zF47;-bD0I{XGVvHQFPpIJe&9yf(x@tKx29({-M%@)b4Xjs5%V1KP2CP*vF;#mh>#V zdkWFvo;o_`?bH%rX-cTU?aaa6S%krcBWCvLa@V^6D!*_C1*UuI&dv;!J^ioJgrFS8 zuqW*vcoFLPf9`z|m29K{0j!*_oL~feAiYS@+klQ#&W3{jCM)|aH9uGf?XV9vZhtWs z%-|!lq*1{?!!_Fy{)7?sUs*q|Y3Lk*>K#tQl?ey=iV2@_TfKZFx%w&`=)lx^CMwY@ z=o)a+sM;YOB@_9Fi7Efw4Wkt0-a}4;cKuig%C5eoewSRv)#5=o|L!j%{I#=G<26}3 zq1)mUqqv+th$YzrQLi8z)J0khQSS^fiyFj1nb|Sc&P4k|E};+D0dke9!bV9FfkNye zaJ$5PU$DjK>S7Mjr&JGS8Jt5c zLYSoTx$M9GY&c7P zj$kllRrr$x8BkczxQ97J-|mJ9@!kg~^71H; z@^$^fH!|UBKhMQ&5kM4~CJ*58oj(J}S}7ds&Fi5%&oZgDn(KXxZDlkTRl|58EjWR44!OKjKxD z&(C#{-dIunGob^uF~`LT_Qhd#UL7B#9N#oin1gjOaw=<-5U?@+BuX?IJ^-@!TY7Y% zh0*?*DpANzpgcXRCDL&8{{<@u)c5Q%f;zwIJ zrMlrPHBlI0TSwEHFeiVUM_G)yOgSmMKh&V71M}&%@h~gg^~UvYM&FFH7D&`YC_})Pk!6Li_VJZB&jx&xF&r+fK{$ygf|6K=okEv>tpYQ9A7wQ~LuQ zX}se(c;H!k(cPZ6UkSE*DljH7RU&@p(FGU~==a+W15uCWwhOu%F^CUUt$z-;-NH1U37g4C|JJB}U4 zxhTMx*m0wY3&vayHIalgi2^D1&-pG;(Ftfq7?Z1F&-s`%h%S-(I48qkOzJqyom|44 z6UJny*vXWxk96SouUx@;fr?&Or3~1Ni72Pl*_$SA7KDQH0#&M}bp8=0J|&JQCut7y zc+>N~1gsaR=t+p0aT*iTH(JLzX`%_@xY42@L z4{@H?m~&j9l2|d*yLUNaF(z8cQ%hTww!bG%U;A3B*ttzE*Uo4WEM4rWa~KmvX4l$7 z&ANvoNiWV_Vd4zoEm$Uwn`APk5{$+~k*TjvVoq(G1MP2iMAXSvU?NlgnDw~U&pwJw zT~&Mg;MAQQ3HtOPI1vTAa(Rr2BC}%ct!7OFC%ypfiu}18=UQ>NAV-rFnfbuUYb%eO zB~G1Gq!;I5kk-#Y$mjaqJB-Oc$)w$h%$_r!(=d^pA!<83;58;bBVk*))onKtoZzXE z&yB#O%JC^_%pf#!jrYVdpw@BS_Ym&6^Etg{iuEuj`pH0Y8xu1QGitVNAO57`T;#~+ zXtkl#-H6K!#wk{;yy{VQQ$I_wUmiY14Or^msPSW;n3h8fNV|SM^aB6`{8{+QEVoy*n07E@O(HIQdq9aF0d*9Y3=$T>}cPF)u1c zwLYer95{!M*33)eN3~F}zqSgM;#teNF z>_zD%25~Gn$6H1Fgv*#eFvu)UHmu2kb0pO|%Wn~rF=pt|nz$rpAU3u3kkE%W{0Opnk&d3DI--VBhw)75t5CcwDV~VfE zkBhccCjy;i93L6l`8l=9r7AKy15Q3;3UQw7{FF>59`0Y5!WkUEn17Nj$%wf-72-Jjr%6O{1W24+(Dkm@$Q@S(kq@~dM8%d z6*uYELFsA6!=h4+*LSHF&o;{*=t1c98)Yrmw_Zr@|1CF7Afm2@X0)0x6ocU% zp;P7*L1E%uNx~2Ds>ebL)eA4Q@Bc=_4%!V?#>Qxqevj~RlKdz6IlCJlLZTPzsiu?PezWgML<=NzEKeTlam7>L>MIz^?<2^1#sEI(N62&l4D2W+0Aax5x<=F2H` z%g?L=0kOP^4ui<4xbQ>eVLBDwGTT3NQgtn|0Vu9H1H`+%ha==)biO7`7!2a@;%p22 kJJzct@F0X_`M)OL0qtIKw2(NPegFUf07*qoM6N<$f_ez0CjbBd literal 0 HcmV?d00001 diff --git a/public/js/index.js b/public/js/index.js index 8a85ad9157..3b15ad8f18 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) { @@ -2219,6 +2254,7 @@ $(document).ready(function () { initIssueList(); initWipTitle(); initPullRequestReview(); + initRepoStatusChecker(); // Repo clone url. if ($('#repo-clone-url').length > 0) { diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index d8b06862a5..08c0635bc3 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -398,8 +398,8 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { } var opts = migrations.MigrateOptions{ - RemoteURL: remoteAddr, - Name: form.RepoName, + CloneAddr: remoteAddr, + RepoName: form.RepoName, Description: form.Description, Private: form.Private || setting.Repository.ForcePrivate, Mirror: form.Mirror, diff --git a/routers/init.go b/routers/init.go index 1efddcfaa6..c37bbeb6b0 100644 --- a/routers/init.go +++ b/routers/init.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/ssh" + "code.gitea.io/gitea/modules/task" "code.gitea.io/gitea/services/mailer" mirror_service "code.gitea.io/gitea/services/mirror" @@ -102,6 +103,9 @@ func GlobalInit() { mirror_service.InitSyncMirrors() models.InitDeliverHooks() models.InitTestPullRequests() + if err := task.Init(); err != nil { + log.Fatal("Failed to initialize task scheduler: %v", err) + } } if setting.EnableSQLite3 { log.Info("SQLite3 Supported") diff --git a/routers/private/serv.go b/routers/private/serv.go index 71c0f6ea2c..c4508b4cb5 100644 --- a/routers/private/serv.go +++ b/routers/private/serv.go @@ -119,6 +119,15 @@ func ServCommand(ctx *macaron.Context) { repo.OwnerName = ownerName results.RepoID = repo.ID + if repo.IsBeingCreated() { + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": "Repository is being created, you could retry after it finished", + }) + return + } + // We can shortcut at this point if the repo is a mirror if mode > models.AccessModeRead && repo.IsMirror { ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ diff --git a/routers/repo/repo.go b/routers/repo/repo.go index b67384d721..bfd0c771b0 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/migrations" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/task" "code.gitea.io/gitea/modules/util" "github.com/unknwon/com" @@ -133,8 +134,6 @@ func Create(ctx *context.Context) { func handleCreateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form interface{}) { switch { - case migrations.IsRateLimitError(err): - ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form) case models.IsErrReachLimitOfRepo(err): ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form) case models.IsErrRepoAlreadyExist(err): @@ -221,6 +220,40 @@ func Migrate(ctx *context.Context) { ctx.HTML(200, tplMigrate) } +func handleMigrateError(ctx *context.Context, owner *models.User, err error, name string, tpl base.TplName, form *auth.MigrateRepoForm) { + switch { + case migrations.IsRateLimitError(err): + ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form) + case migrations.IsTwoFactorAuthError(err): + ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form) + case models.IsErrReachLimitOfRepo(err): + ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", owner.MaxCreationLimit()), tpl, form) + case models.IsErrRepoAlreadyExist(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form) + case models.IsErrNameReserved(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tpl, form) + case models.IsErrNamePatternNotAllowed(err): + ctx.Data["Err_RepoName"] = true + ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tpl, form) + default: + remoteAddr, _ := form.ParseRemoteAddr(owner) + err = util.URLSanitizedError(err, remoteAddr) + if strings.Contains(err.Error(), "Authentication failed") || + strings.Contains(err.Error(), "Bad credentials") || + strings.Contains(err.Error(), "could not read Username") { + ctx.Data["Err_Auth"] = true + ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form) + } else if strings.Contains(err.Error(), "fatal:") { + ctx.Data["Err_CloneAddr"] = true + ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form) + } else { + ctx.ServerError(name, err) + } + } +} + // MigratePost response for migrating from external git repository func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { ctx.Data["Title"] = ctx.Tr("new_migrate") @@ -258,8 +291,8 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { } var opts = migrations.MigrateOptions{ - RemoteURL: remoteAddr, - Name: form.RepoName, + CloneAddr: remoteAddr, + RepoName: form.RepoName, Description: form.Description, Private: form.Private || setting.Repository.ForcePrivate, Mirror: form.Mirror, @@ -282,47 +315,19 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { opts.Releases = false } - repo, err := migrations.MigrateRepository(ctx.User, ctxUser.Name, opts) - if err == nil { - notification.NotifyCreateRepository(ctx.User, ctxUser, repo) - - log.Trace("Repository migrated [%d]: %s/%s successfully", repo.ID, ctxUser.Name, form.RepoName) - ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + form.RepoName) + err = models.CheckCreateRepository(ctx.User, ctxUser, opts.RepoName) + if err != nil { + handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) return } - switch { - case models.IsErrReachLimitOfRepo(err): - ctx.RenderWithErr(ctx.Tr("repo.form.reach_limit_of_creation", ctxUser.MaxCreationLimit()), tplMigrate, &form) - case models.IsErrNameReserved(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(models.ErrNameReserved).Name), tplMigrate, &form) - case models.IsErrRepoAlreadyExist(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplMigrate, &form) - case models.IsErrNamePatternNotAllowed(err): - ctx.Data["Err_RepoName"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplMigrate, &form) - case migrations.IsRateLimitError(err): - ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tplMigrate, &form) - case migrations.IsTwoFactorAuthError(err): - ctx.Data["Err_Auth"] = true - ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tplMigrate, &form) - default: - // remoteAddr may contain credentials, so we sanitize it - err = util.URLSanitizedError(err, remoteAddr) - if strings.Contains(err.Error(), "Authentication failed") || - strings.Contains(err.Error(), "Bad credentials") || - strings.Contains(err.Error(), "could not read Username") { - ctx.Data["Err_Auth"] = true - ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tplMigrate, &form) - } else if strings.Contains(err.Error(), "fatal:") { - ctx.Data["Err_CloneAddr"] = true - ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tplMigrate, &form) - } else { - ctx.ServerError("MigratePost", err) - } + err = task.MigrateRepository(ctx.User, ctxUser, opts) + if err == nil { + ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + opts.RepoName) + return } + + handleMigrateError(ctx, ctxUser, err, "MigratePost", tplMigrate, &form) } // Action response for actions to a repository @@ -460,3 +465,19 @@ func Download(ctx *context.Context) { ctx.ServeFile(archivePath, ctx.Repo.Repository.Name+"-"+refName+ext) } + +// Status returns repository's status +func Status(ctx *context.Context) { + task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) + if err != nil { + ctx.JSON(500, map[string]interface{}{ + "err": err, + }) + return + } + + ctx.JSON(200, map[string]interface{}{ + "status": ctx.Repo.Repository.Status, + "err": task.Errors, + }) +} diff --git a/routers/repo/view.go b/routers/repo/view.go index 1967b511ca..c4e6a69220 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -11,6 +11,7 @@ import ( "fmt" gotemplate "html/template" "io/ioutil" + "net/url" "path" "strings" @@ -31,6 +32,7 @@ const ( tplRepoHome base.TplName = "repo/home" tplWatchers base.TplName = "repo/watchers" tplForks base.TplName = "repo/forks" + tplMigrating base.TplName = "repo/migrating" ) func renderDirectory(ctx *context.Context, treeLink string) { @@ -356,9 +358,37 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st } } +func safeURL(address string) string { + u, err := url.Parse(address) + if err != nil { + return address + } + u.User = nil + return u.String() +} + // Home render repository home page func Home(ctx *context.Context) { if len(ctx.Repo.Units) > 0 { + if ctx.Repo.Repository.IsBeingCreated() { + task, err := models.GetMigratingTask(ctx.Repo.Repository.ID) + if err != nil { + ctx.ServerError("models.GetMigratingTask", err) + return + } + cfg, err := task.MigrateConfig() + if err != nil { + ctx.ServerError("task.MigrateConfig", err) + return + } + + ctx.Data["Repo"] = ctx.Repo + ctx.Data["MigrateTask"] = task + ctx.Data["CloneAddr"] = safeURL(cfg.CloneAddr) + ctx.HTML(200, tplMigrating) + return + } + var firstUnit *models.Unit for _, repoUnit := range ctx.Repo.Units { if repoUnit.Type == models.UnitTypeCode { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 11f2029226..8dfcdb9c9b 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -845,6 +845,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/archive/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.Download) + m.Get("/status", reqRepoCodeReader, repo.Status) + m.Group("/branches", func() { m.Get("", repo.Branches) }, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader) diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 76bd4c72f7..9ad11b7265 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/structs" release_service "code.gitea.io/gitea/services/release" "github.com/stretchr/testify/assert" @@ -26,16 +27,26 @@ func TestRelease_MirrorDelete(t *testing.T) { repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) repoPath := models.RepoPath(user.Name, repo.Name) - migrationOptions := models.MigrateRepoOptions{ - Name: "test_mirror", - Description: "Test mirror", - IsPrivate: false, - IsMirror: true, - RemoteAddr: repoPath, - Wiki: true, - SyncReleasesWithTags: true, + opts := structs.MigrateRepoOption{ + RepoName: "test_mirror", + Description: "Test mirror", + Private: false, + Mirror: true, + CloneAddr: repoPath, + Wiki: true, + Releases: false, } - mirror, err := models.MigrateRepository(user, user, migrationOptions) + + mirrorRepo, err := models.CreateRepository(user, user, models.CreateRepoOptions{ + Name: opts.RepoName, + Description: opts.Description, + IsPrivate: opts.Private, + IsMirror: opts.Mirror, + Status: models.RepositoryBeingMigrated, + }) + assert.NoError(t, err) + + mirror, err := models.MigrateRepositoryGitData(user, user, mirrorRepo, opts) assert.NoError(t, err) gitRepo, err := git.OpenRepository(repoPath) diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index fc7f1b660c..9fb3e32899 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -16,93 +16,95 @@ {{if .IsMirror}}
{{$.i18n.Tr "repo.mirror_from"}} {{MirrorAddress $.Mirror}}
{{end}} {{if .IsFork}}
{{$.i18n.Tr "repo.forked_from"}} {{SubStr .BaseRepo.RelLink 1 -1}}
{{end}}
- +{{end}} +
+ {{if not .Repository.IsBeingCreated}} + -
-
-{{end}} - -
- + {{end}}
-
diff --git a/templates/repo/migrating.tmpl b/templates/repo/migrating.tmpl new file mode 100644 index 0000000000..34031d5653 --- /dev/null +++ b/templates/repo/migrating.tmpl @@ -0,0 +1,31 @@ +{{template "base/head" .}} +
+ {{template "repo/header" .}} +
+
+
+ {{template "base/alert" .}} +
+
+
+
+ +
+
+
+
+
+
+

{{.i18n.Tr "repo.migrate.migrating" .CloneAddr | Safe}}

+
+
+

{{.i18n.Tr "repo.migrate.migrating_failed" .CloneAddr | Safe}}

+
+
+
+
+
+
+
+
+{{template "base/footer" .}} From 300d9a1c709a0addde7c77efa7153f66ff6748d8 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 13 Oct 2019 15:35:19 +0100 Subject: [PATCH 043/154] Fixes #8369: Create .ssh dir as necessary (#8486) * Ensure .ssh dir exists before rewriting public keys * Ensure .ssh dir exists before appending to authorized_keys * Log the error because it would be useful to know where it is trying to MkdirAll * Only try to create RootPath if it's not empty --- models/ssh_key.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/models/ssh_key.go b/models/ssh_key.go index b7c5b4fe6e..d1132bf0c6 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -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) From f858b89b130352265d59b5d46af626373796b74e Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 13 Oct 2019 14:37:37 +0000 Subject: [PATCH 044/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_es-ES.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index dbfa462217..c29476987d 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1354,6 +1354,7 @@ 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_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 @@ -1456,6 +1457,7 @@ settings.options=Organización settings.full_name=Nombre completo settings.website=Página web settings.location=Localización +settings.permission=Permisos settings.visibility=Visibilidad settings.visibility.public=Público settings.visibility.limited=Limitado (Visible sólo para los usuarios registrados) From c888ebfba713b4d5be88c427a34d29153be52076 Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 13 Oct 2019 17:29:08 +0100 Subject: [PATCH 045/154] IsBranchExist: return false if provided name is empty (#8485) * IsBranchExist: return false if provided name is empty * Ensure that the reference returned is actually of a valid type --- modules/git/repo_branch.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 3e1261d294..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. From c23cf4c97c82c7f09a85459b676777b597a99c8d Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 13 Oct 2019 16:31:19 +0000 Subject: [PATCH 046/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_es-ES.ini | 12 ++++++++++++ options/locale/locale_lv-LV.ini | 2 ++ 2 files changed, 14 insertions(+) diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index c29476987d..f5355a488f 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -568,6 +568,7 @@ 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) @@ -1208,6 +1209,9 @@ 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.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. @@ -1257,6 +1261,7 @@ 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.event_repository=Repositorio settings.event_repository_desc=Repositorio creado o eliminado. settings.active=Activo @@ -1307,6 +1312,8 @@ 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_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: @@ -1354,6 +1361,10 @@ 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 @@ -1704,6 +1715,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'. diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 21a1b7e269..f8a21ac0f0 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -632,6 +632,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 From ba716705b5420707fac05d362f986c9aee359ad2 Mon Sep 17 00:00:00 2001 From: Benson Muite Date: Sun, 13 Oct 2019 23:07:30 +0300 Subject: [PATCH 047/154] Update seek-help.en-us.md (#8487) Update link to Mandarin help --- docs/content/doc/help/seek-help.en-us.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/doc/help/seek-help.en-us.md b/docs/content/doc/help/seek-help.en-us.md index e9d0211029..058a5bb592 100644 --- a/docs/content/doc/help/seek-help.en-us.md +++ b/docs/content/doc/help/seek-help.en-us.md @@ -33,4 +33,4 @@ If you found a bug, please create an [issue on GitHub](https://github.com/go-git ## Chinese Support -Support for the Chinese language is provided at [gocn.io](https://gocn.io/topic/Gitea). +Support for the Chinese language is provided at [gocn.vip](https://gocn.vip/topic/gitea). From 0c680f337d7ab71f530ccb41a499aa9371b7e161 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 13 Oct 2019 20:23:11 +0000 Subject: [PATCH 048/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_es-ES.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index f5355a488f..b5fa9b3bdd 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1209,6 +1209,7 @@ 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.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. From 6e3f51098b29cd5c61d62732a42a7554cbc8cc2f Mon Sep 17 00:00:00 2001 From: Benson Muite Date: Mon, 14 Oct 2019 00:36:09 +0300 Subject: [PATCH 049/154] Update seek-help.zh-cn.md (#8488) Update link to Mandarin help forum --- docs/content/doc/help/seek-help.zh-cn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/doc/help/seek-help.zh-cn.md b/docs/content/doc/help/seek-help.zh-cn.md index ac62b9b8cf..7e4e2f1beb 100644 --- a/docs/content/doc/help/seek-help.zh-cn.md +++ b/docs/content/doc/help/seek-help.zh-cn.md @@ -18,6 +18,6 @@ menu: 如果您在使用或者开发过程中遇到问题,请到以下渠道咨询: - 到[Github issue](https://github.com/go-gitea/gitea/issues)提问(因为项目维护人员来自世界各地,为保证沟通顺畅,请使用英文提问) -- 中文问题到[gocn.io](https://gocn.io/topic/Gitea)提问 +- 中文问题到[gocn.vip](https://gocn.vip/topic/gitea)提问 - 访问 [Discord server - 英文](https://discord.gg/NsatcWJ) - 加入 QQ群 328432459 获得进一步的支持 From 15809d81f7d36759f289b941352a9754611c5dba Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Sun, 13 Oct 2019 19:29:10 -0300 Subject: [PATCH 050/154] Rewrite reference processing code in preparation for opening/closing from comment references (#8261) * Add a markdown stripper for mentions and xrefs * Improve comments * Small code simplification * Move reference code to modules/references * Fix typo * Make MarkdownStripper return [][]byte * Implement preliminary keywords parsing * Add FIXME comment * Fix comment * make fmt * Fix permissions check * Fix text assumptions * Fix imports * Fix lint, fmt * Fix unused import * Add missing export comment * Bypass revive on implemented interface * Move mdstripper into its own package * Support alphanumeric patterns * Refactor FindAllMentions * Move mentions test to references * Parse mentions from reference package * Refactor code to implement renderizable references * Fix typo * Move patterns and tests to the references package * Fix nil reference * Preliminary rendering attempt of closing keywords * Normalize names, comments, general tidy-up * Add CSS style for action keywords * Fix permission for admin and owner * Fix golangci-lint * Fix golangci-lint --- integrations/issue_test.go | 13 +- models/action.go | 181 +++-------- models/action_test.go | 53 +-- models/issue_comment.go | 11 +- models/issue_xref.go | 109 +++---- modules/markup/html.go | 136 ++++---- modules/markup/html_internal_test.go | 92 ------ modules/markup/mdstripper/mdstripper.go | 260 +++++++++++++++ modules/markup/mdstripper/mdstripper_test.go | 71 ++++ modules/markup/sanitizer.go | 3 + modules/references/references.go | 322 +++++++++++++++++++ modules/references/references_test.go | 296 +++++++++++++++++ public/css/index.css | 1 + public/less/_repository.less | 5 + services/mailer/mail_comment.go | 4 +- services/mailer/mail_issue.go | 4 +- 16 files changed, 1123 insertions(+), 438 deletions(-) create mode 100644 modules/markup/mdstripper/mdstripper.go create mode 100644 modules/markup/mdstripper/mdstripper_test.go create mode 100644 modules/references/references.go create mode 100644 modules/references/references_test.go 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/models/action.go b/models/action.go index 87088101f9..2d2999f880 100644 --- a/models/action.go +++ b/models/action.go @@ -10,15 +10,14 @@ import ( "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 +53,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 +327,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 +452,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 +464,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 +478,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 +495,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 } } 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/issue_comment.go b/models/issue_comment.go index e8043c1ec7..7d38302b98 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -13,6 +13,7 @@ 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" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" @@ -144,10 +145,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:"-"` @@ -773,7 +774,7 @@ type CreateCommentOptions struct { RefRepoID int64 RefIssueID int64 RefCommentID int64 - RefAction XRefAction + RefAction references.XRefAction RefIsPull bool } diff --git a/models/issue_xref.go b/models/issue_xref.go index 1cc0bcfe6a..141a7e0e8c 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 -) - 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/modules/markup/html.go b/modules/markup/html.go index f07993bc4c..fc823b1f30 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,17 +92,6 @@ 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:] - } - return ret -} - // IsSameDomain checks if given url string has the same hostname as current Gitea instance func IsSameDomain(s string) bool { if strings.HasPrefix(s, "/") { @@ -142,7 +124,6 @@ var defaultProcessors = []processor{ linkProcessor, mentionProcessor, issueIndexPatternProcessor, - crossReferenceIssueIndexPatternProcessor, sha1CurrentPatternProcessor, emailAddressProcessor, } @@ -183,7 +164,6 @@ var commitMessageProcessors = []processor{ linkProcessor, mentionProcessor, issueIndexPatternProcessor, - crossReferenceIssueIndexPatternProcessor, sha1CurrentPatternProcessor, emailAddressProcessor, } @@ -217,7 +197,6 @@ var commitMessageSubjectProcessors = []processor{ linkProcessor, mentionProcessor, issueIndexPatternProcessor, - crossReferenceIssueIndexPatternProcessor, sha1CurrentPatternProcessor, } @@ -330,6 +309,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 +374,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 +395,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 +407,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 +602,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/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..fd6f90b2ab 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -38,6 +38,9 @@ func NewSanitizer() { // Custom URL-Schemes sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...) + + // Allow keyword markup + sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^` + keywordClass + `$`)).OnElements("span") }) } 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/public/css/index.css b/public/css/index.css index 0efd787122..fda26f4e08 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -878,6 +878,7 @@ 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} diff --git a/public/less/_repository.less b/public/less/_repository.less index 0527759ed4..5f6a7fbd97 100644 --- a/public/less/_repository.less +++ b/public/less/_repository.less @@ -2384,6 +2384,11 @@ tbody.commit-list { padding-bottom: 8px; } +.issue-keyword { + border-bottom: 1px dotted #959da5; + display: inline-block; +} + .file-header { display: flex; justify-content: space-between; diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go index f64db04fff..d306c14f42 100644 --- a/services/mailer/mail_comment.go +++ b/services/mailer/mail_comment.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/references" ) // MailParticipantsComment sends new comment emails to repository watchers @@ -19,7 +19,7 @@ func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue } func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType models.ActionType, issue *models.Issue) (err error) { - rawMentions := markup.FindAllMentions(c.Content) + rawMentions := references.FindAllMentionsMarkdown(c.Content) userMentions, err := issue.ResolveMentionsByVisibility(ctx, c.Poster, rawMentions) if err != nil { return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", c.IssueID, err) diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index da0249d595..b16323909c 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/setting" "github.com/unknwon/com" @@ -123,7 +123,7 @@ func MailParticipants(issue *models.Issue, doer *models.User, opType models.Acti } func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.User, opType models.ActionType) (err error) { - rawMentions := markup.FindAllMentions(issue.Content) + rawMentions := references.FindAllMentionsMarkdown(issue.Content) userMentions, err := issue.ResolveMentionsByVisibility(ctx, doer, rawMentions) if err != nil { return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", issue.ID, err) From ba201aaa44b19f633fab0c4682d5f97558b3205e Mon Sep 17 00:00:00 2001 From: Antoine GIRARD Date: Mon, 14 Oct 2019 02:38:15 +0200 Subject: [PATCH 051/154] vendor: update mvdan.cc/xurls/v2 to v2.1.0 (#8495) --- go.mod | 2 +- go.sum | 4 ++-- vendor/modules.txt | 2 +- vendor/mvdan.cc/xurls/v2/.travis.yml | 17 ----------------- vendor/mvdan.cc/xurls/v2/README.md | 18 +++++++++++------- vendor/mvdan.cc/xurls/v2/go.mod | 2 ++ vendor/mvdan.cc/xurls/v2/schemes.go | 23 +++++++++++++++++++++++ vendor/mvdan.cc/xurls/v2/tlds.go | 19 +++++-------------- vendor/mvdan.cc/xurls/v2/xurls.go | 10 ++++++---- 9 files changed, 51 insertions(+), 46 deletions(-) delete mode 100644 vendor/mvdan.cc/xurls/v2/.travis.yml diff --git a/go.mod b/go.mod index d2520da739..203b6ce4c1 100644 --- a/go.mod +++ b/go.mod @@ -120,7 +120,7 @@ require ( 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 + 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 diff --git a/go.sum b/go.sum index 3adf91e926..7306966800 100644 --- a/go.sum +++ b/go.sum @@ -812,8 +812,8 @@ 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-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs= strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= diff --git a/vendor/modules.txt b/vendor/modules.txt index 8b2224f54a..30b1370933 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -610,7 +610,7 @@ gopkg.in/testfixtures.v2 gopkg.in/warnings.v0 # gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 -# mvdan.cc/xurls/v2 v2.0.0 +# mvdan.cc/xurls/v2 v2.1.0 mvdan.cc/xurls/v2 # strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 strk.kbt.io/projects/go/libravatar diff --git a/vendor/mvdan.cc/xurls/v2/.travis.yml b/vendor/mvdan.cc/xurls/v2/.travis.yml deleted file mode 100644 index e9bd7a7650..0000000000 --- a/vendor/mvdan.cc/xurls/v2/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: go - -go: - - 1.10.x - - 1.11.x - -go_import_path: mvdan.cc/xurls - -env: - - GO111MODULE=on - -install: true - -script: - - go get -t -d ./... - - go build ./... - - go test ./... diff --git a/vendor/mvdan.cc/xurls/v2/README.md b/vendor/mvdan.cc/xurls/v2/README.md index 5058efe7a7..07fbdb3b25 100644 --- a/vendor/mvdan.cc/xurls/v2/README.md +++ b/vendor/mvdan.cc/xurls/v2/README.md @@ -1,18 +1,20 @@ # xurls [![GoDoc](https://godoc.org/mvdan.cc/xurls?status.svg)](https://godoc.org/mvdan.cc/xurls) -[![Travis](https://travis-ci.org/mvdan/xurls.svg?branch=master)](https://travis-ci.org/mvdan/xurls) -Extract urls from text using regular expressions. Requires Go 1.10.3 or later. +Extract urls from text using regular expressions. Requires Go 1.12 or later. ```go import "mvdan.cc/xurls/v2" func main() { - xurls.Relaxed().FindString("Do gophers live in golang.org?") - // "golang.org" - xurls.Strict().FindAllString("foo.com is http://foo.com/.", -1) - // []string{"http://foo.com/"} + rxRelaxed := xurls.Relaxed() + rxRelaxed.FindString("Do gophers live in golang.org?") // "golang.org" + rxRelaxed.FindString("This string does not have a URL") // "" + + rxStrict := xurls.Strict() + rxStrict.FindAllString("must have scheme: http://foo.com/.", -1) // []string{"http://foo.com/"} + rxStrict.FindAllString("no scheme, no match: foo.com", -1) // []string{} } ``` @@ -20,7 +22,9 @@ Note that the funcs compile regexes, so avoid calling them repeatedly. #### cmd/xurls - go get -u mvdan.cc/xurls/v2/cmd/xurls +To install the tool globally: + + go get mvdan.cc/xurls/cmd/xurls ```shell $ echo "Do gophers live in http://golang.org?" | xurls diff --git a/vendor/mvdan.cc/xurls/v2/go.mod b/vendor/mvdan.cc/xurls/v2/go.mod index 6f0822a737..d9d334543c 100644 --- a/vendor/mvdan.cc/xurls/v2/go.mod +++ b/vendor/mvdan.cc/xurls/v2/go.mod @@ -1 +1,3 @@ module mvdan.cc/xurls/v2 + +go 1.13 diff --git a/vendor/mvdan.cc/xurls/v2/schemes.go b/vendor/mvdan.cc/xurls/v2/schemes.go index 01b7944ae3..e8e6585f47 100644 --- a/vendor/mvdan.cc/xurls/v2/schemes.go +++ b/vendor/mvdan.cc/xurls/v2/schemes.go @@ -12,13 +12,18 @@ var Schemes = []string{ `about`, `acap`, `acct`, + `acd`, `acr`, `adiumxtra`, + `adt`, `afp`, `afs`, `aim`, + `amss`, + `android`, `appdata`, `apt`, + `ark`, `attachment`, `aw`, `barion`, @@ -28,8 +33,11 @@ var Schemes = []string{ `blob`, `bolo`, `browserext`, + `calculator`, `callto`, `cap`, + `cast`, + `casts`, `chrome`, `chrome-extension`, `cid`, @@ -44,6 +52,7 @@ var Schemes = []string{ `conti`, `crid`, `cvs`, + `dab`, `data`, `dav`, `diaspora`, @@ -54,6 +63,9 @@ var Schemes = []string{ `dlna-playsingle`, `dns`, `dntp`, + `dpp`, + `drm`, + `drop`, `dtn`, `dvb`, `ed2k`, @@ -66,8 +78,11 @@ var Schemes = []string{ `file`, `filesystem`, `finger`, + `first-run-pen-experience`, `fish`, + `fm`, `ftp`, + `fuchsia-pkg`, `geo`, `gg`, `git`, @@ -112,6 +127,8 @@ var Schemes = []string{ `lastfm`, `ldap`, `ldaps`, + `leaptofrogans`, + `lorawan`, `lvlt`, `magnet`, `mailserver`, @@ -129,9 +146,11 @@ var Schemes = []string{ `moz`, `ms-access`, `ms-browser-extension`, + `ms-calculator`, `ms-drive-to`, `ms-enrollment`, `ms-excel`, + `ms-eyecontrolspeech`, `ms-gamebarservices`, `ms-gamingoverlay`, `ms-getoffice`, @@ -141,6 +160,7 @@ var Schemes = []string{ `ms-lockscreencomponent-config`, `ms-media-stream-id`, `ms-mixedrealitycapture`, + `ms-mobileplans`, `ms-officeapp`, `ms-people`, `ms-project`, @@ -186,6 +206,7 @@ var Schemes = []string{ `msnim`, `msrp`, `msrps`, + `mss`, `mtqp`, `mumble`, `mupdate`, @@ -205,6 +226,7 @@ var Schemes = []string{ `pack`, `palm`, `paparazzi`, + `payto`, `pkcs11`, `platform`, `pop`, @@ -213,6 +235,7 @@ var Schemes = []string{ `proxy`, `pwid`, `psyc`, + `pttp`, `qb`, `query`, `redis`, diff --git a/vendor/mvdan.cc/xurls/v2/tlds.go b/vendor/mvdan.cc/xurls/v2/tlds.go index 084ab84d46..c98ce508d6 100644 --- a/vendor/mvdan.cc/xurls/v2/tlds.go +++ b/vendor/mvdan.cc/xurls/v2/tlds.go @@ -24,7 +24,6 @@ var TLDs = []string{ `accountant`, `accountants`, `aco`, - `active`, `actor`, `ad`, `adac`, @@ -154,7 +153,6 @@ var TLDs = []string{ `bj`, `black`, `blackfriday`, - `blanco`, `blockbuster`, `blog`, `bloomberg`, @@ -163,7 +161,6 @@ var TLDs = []string{ `bms`, `bmw`, `bn`, - `bnl`, `bnpparibas`, `bo`, `boats`, @@ -307,6 +304,7 @@ var TLDs = []string{ `coupon`, `coupons`, `courses`, + `cpa`, `cr`, `credit`, `creditcard`, @@ -370,7 +368,6 @@ var TLDs = []string{ `doctor`, `dodge`, `dog`, - `doha`, `domains`, `dot`, `download`, @@ -379,7 +376,6 @@ var TLDs = []string{ `dubai`, `duck`, `dunlop`, - `duns`, `dupont`, `durban`, `dvag`, @@ -400,7 +396,6 @@ var TLDs = []string{ `engineer`, `engineering`, `enterprises`, - `epost`, `epson`, `equipment`, `er`, @@ -496,6 +491,7 @@ var TLDs = []string{ `games`, `gap`, `garden`, + `gay`, `gb`, `gbiz`, `gd`, @@ -588,7 +584,6 @@ var TLDs = []string{ `homes`, `homesense`, `honda`, - `honeywell`, `horse`, `hospital`, `host`, @@ -642,7 +637,6 @@ var TLDs = []string{ `ir`, `irish`, `is`, - `iselect`, `ismaili`, `ist`, `istanbul`, @@ -752,6 +746,7 @@ var TLDs = []string{ `lixil`, `lk`, `llc`, + `llp`, `loan`, `loans`, `locker`, @@ -827,7 +822,6 @@ var TLDs = []string{ `mo`, `mobi`, `mobile`, - `mobily`, `moda`, `moe`, `moi`, @@ -1161,21 +1155,19 @@ var TLDs = []string{ `sony`, `soy`, `space`, - `spiegel`, `sport`, `spot`, `spreadbetting`, `sr`, `srl`, `srt`, + `ss`, `st`, `stada`, `staples`, `star`, - `starhub`, `statebank`, `statefarm`, - `statoil`, `stc`, `stcgroup`, `stockholm`, @@ -1391,7 +1383,6 @@ var TLDs = []string{ `zara`, `zero`, `zip`, - `zippo`, `zm`, `zone`, `zuerich`, @@ -1449,7 +1440,7 @@ var TLDs = []string{ `كوم`, `مصر`, `مليسيا`, - `موبايلي`, + `موريتانيا`, `موقع`, `همراه`, `پاكستان`, diff --git a/vendor/mvdan.cc/xurls/v2/xurls.go b/vendor/mvdan.cc/xurls/v2/xurls.go index d6279ae60b..7244c709a0 100644 --- a/vendor/mvdan.cc/xurls/v2/xurls.go +++ b/vendor/mvdan.cc/xurls/v2/xurls.go @@ -19,9 +19,9 @@ const ( iriChar = letter + mark + number currency = `\p{Sc}` otherSymb = `\p{So}` - endChar = iriChar + `/\-+_&~*%=#` + currency + otherSymb + endChar = iriChar + `/\-+&~%=#` + currency + otherSymb otherPunc = `\p{Po}` - midChar = endChar + `|` + otherPunc + midChar = endChar + "_*" + otherPunc wellParen = `\([` + midChar + `]*(\([` + midChar + `]*\)[` + midChar + `]*)*\)` wellBrack = `\[[` + midChar + `]*(\[[` + midChar + `]*\][` + midChar + `]*)*\]` wellBrace = `\{[` + midChar + `]*(\{[` + midChar + `]*\}[` + midChar + `]*)*\}` @@ -72,9 +72,11 @@ func strictExp() string { } func relaxedExp() string { - site := domain + `(?i)` + anyOf(append(TLDs, PseudoTLDs...)...) + `(?-i)` + punycode := `xn--[a-z0-9-]+` + knownTLDs := anyOf(append(TLDs, PseudoTLDs...)...) + site := domain + `(?i)(` + punycode + `|` + knownTLDs + `)(?-i)` hostName := `(` + site + `|` + ipAddr + `)` - webURL := hostName + port + `(/|/` + pathCont + `?|\b|$)` + webURL := hostName + port + `(/|/` + pathCont + `?|\b|(?m)$)` return strictExp() + `|` + webURL } From e3e44a59d01da3af2be3a830f4a90394e7af4ff4 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 14 Oct 2019 14:10:42 +0800 Subject: [PATCH 052/154] Update migrated repositories' issues/comments/prs poster id if user has a github external user saved (#7751) * update migrated issues/comments when login as github * add get userid when migrating or login with github oauth2 * fix lint * add migrations for repository service type * fix build * remove unnecessary dependencies on migrations * add cron task to update migrations poster ids and fix posterid when migrating * fix lint * fix lint * improve code * fix lint * improve code * replace releases publish id to actual author id * fix import * fix bug * fix lint * fix rawdata definition * fix some bugs * fix error message --- custom/conf/app.ini.sample | 5 + .../doc/advanced/config-cheat-sheet.en-us.md | 4 + .../doc/advanced/config-cheat-sheet.zh-cn.md | 6 +- models/external_login_user.go | 143 +++++++++-- models/issue.go | 16 +- models/issue_comment.go | 21 ++ models/migrations/migrations.go | 2 + models/migrations/v100.go | 83 ++++++ models/release.go | 14 + models/repo.go | 22 +- modules/cron/cron.go | 23 +- modules/migrations/base/downloader.go | 3 + modules/migrations/gitea.go | 239 +++++++++++++----- modules/migrations/github.go | 8 +- modules/migrations/migrate.go | 7 + modules/migrations/update.go | 59 +++++ modules/setting/cron.go | 8 + modules/structs/repo.go | 39 +++ routers/api/v1/repo/repo.go | 37 ++- routers/user/auth.go | 94 ++++--- services/externalaccount/user.go | 66 +++++ 21 files changed, 740 insertions(+), 159 deletions(-) create mode 100644 models/migrations/v100.go create mode 100644 modules/migrations/update.go create mode 100644 services/externalaccount/user.go diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index dd14089d2b..fd8d928ede 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -690,6 +690,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 = 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 ed34be032b..b927793a50 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -419,6 +419,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. 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 01ba821a47..ab73e2059e 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -196,7 +196,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`) diff --git a/models/external_login_user.go b/models/external_login_user.go index 21a3cbbd31..5058fd1b4b 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 + AccessTokenSecret string + RefreshToken string + 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). + Asc("id"). + 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, 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/issue.go b/models/issue.go index 8ce7d496ab..fc675a3ffb 100644 --- a/models/issue.go +++ b/models/issue.go @@ -14,6 +14,7 @@ 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" @@ -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:"-"` @@ -1947,3 +1948,16 @@ func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, menti return } + +// UpdateIssuesMigrationsByType updates all migrated repositories' issues from gitServiceType to replace originalAuthorID to posterID +func UpdateIssuesMigrationsByType(gitServiceType structs.GitServiceType, originalAuthorID, 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_comment.go b/models/issue_comment.go index 7d38302b98..3a090c3b19 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -14,6 +14,7 @@ import ( "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" @@ -1022,3 +1023,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, 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/migrations/migrations.go b/models/migrations/migrations.go index ef5cd377a6..60a416c6e9 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -254,6 +254,8 @@ var migrations = []Migration{ 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), } // Migrate database to current version diff --git a/models/migrations/v100.go b/models/migrations/v100.go new file mode 100644 index 0000000000..ac3b73e2ad --- /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" + + "github.com/go-xorm/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/release.go b/models/release.go index 243cc2fa3c..03685e0a44 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, 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 23b1c2ef52..aa2cf06f32 100644 --- a/models/repo.go +++ b/models/repo.go @@ -32,6 +32,7 @@ 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" @@ -137,16 +138,17 @@ const ( // 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 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/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/gitea.go b/modules/migrations/gitea.go index ab3b0b9f69..2452a7a883 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -34,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 @@ -52,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), } } @@ -109,13 +112,15 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate } r, err = models.MigrateRepositoryGitData(g.doer, owner, r, structs.MigrateRepoOption{ - RepoName: g.repoName, - Description: repo.Description, - Mirror: repo.IsMirror, - CloneAddr: remoteAddr, - Private: repo.IsPrivate, - Wiki: opts.Wiki, - Releases: opts.Releases, // if didn't get releases, then sync them from tags + 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 @@ -193,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 @@ -284,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()) } @@ -331,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 } @@ -355,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 { @@ -460,6 +544,40 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR head = pr.Head.Ref } + 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()), + } + + 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, @@ -470,22 +588,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*models.PullR Index: pr.Number, HasMerged: pr.Merged, - 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()), - }, + Issue: &issue, } if pullRequest.Issue.IsClosed && pr.Closed != nil { diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 1c5d96c03d..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" @@ -39,7 +40,7 @@ func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error 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 @@ -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 3f5c0d1118..bbc1dc2d56 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -11,6 +11,7 @@ import ( "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 @@ -30,6 +31,7 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt var ( downloader base.Downloader uploader = NewGiteaLocalUploader(doer, ownerName, opts.RepoName) + theFactory base.DownloaderFactory ) for _, factory := range factories { @@ -40,6 +42,7 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt if err != nil { return nil, err } + theFactory = factory break } } @@ -52,10 +55,14 @@ func MigrateRepository(doer *models.User, ownerName string, opts base.MigrateOpt opts.Comments = false opts.Issues = false opts.PullRequests = false + 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) diff --git a/modules/migrations/update.go b/modules/migrations/update.go new file mode 100644 index 0000000000..df626ddd95 --- /dev/null +++ b/modules/migrations/update.go @@ -0,0 +1,59 @@ +// 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 ( + "strconv" + + "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, err := strconv.ParseInt(user.ExternalID, 10, 64) + if err != nil { + log.Warn("Parse externalUser %#v 's userID failed: %v", user, err) + continue + } + 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/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/structs/repo.go b/modules/structs/repo.go index 57f1768a0b..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 @@ -166,6 +203,8 @@ type MigrateRepoOption struct { Mirror bool `json:"mirror"` Private bool `json:"private"` Description string `json:"description"` + OriginalURL string + GitServiceType GitServiceType Wiki bool Issues bool Milestones bool diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 08c0635bc3..a4417107ee 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -8,6 +8,7 @@ package repo import ( "fmt" "net/http" + "net/url" "strings" "code.gitea.io/gitea/models" @@ -17,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/migrations" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" @@ -397,21 +399,28 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { return } + var gitServiceType = structs.PlainGitService + u, err := url.Parse(remoteAddr) + if err == nil && strings.EqualFold(u.Host, "github.com") { + gitServiceType = structs.GithubService + } + var opts = migrations.MigrateOptions{ - CloneAddr: remoteAddr, - RepoName: form.RepoName, - Description: form.Description, - Private: form.Private || setting.Repository.ForcePrivate, - Mirror: form.Mirror, - AuthUsername: form.AuthUsername, - AuthPassword: form.AuthPassword, - Wiki: form.Wiki, - Issues: form.Issues, - Milestones: form.Milestones, - Labels: form.Labels, - Comments: true, - PullRequests: form.PullRequests, - Releases: form.Releases, + CloneAddr: remoteAddr, + RepoName: form.RepoName, + Description: form.Description, + Private: form.Private || setting.Repository.ForcePrivate, + Mirror: form.Mirror, + AuthUsername: form.AuthUsername, + AuthPassword: form.AuthPassword, + Wiki: form.Wiki, + Issues: form.Issues, + Milestones: form.Milestones, + Labels: form.Labels, + Comments: true, + PullRequests: form.PullRequests, + Releases: form.Releases, + GitServiceType: gitServiceType, } if opts.Mirror { opts.Issues = false diff --git a/routers/user/auth.go b/routers/user/auth.go index 3def867f64..212d535a06 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/mailer" "gitea.com/macaron/captcha" @@ -277,7 +278,7 @@ func TwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) { return } - err = models.LinkAccountToUser(u, gothUser.(goth.User)) + err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User)) if err != nil { ctx.ServerError("UserSignIn", err) return @@ -452,7 +453,7 @@ func U2FSign(ctx *context.Context, signResp u2f.SignResponse) { return } - err = models.LinkAccountToUser(user, gothUser.(goth.User)) + err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User)) if err != nil { ctx.ServerError("UserSignIn", err) return @@ -601,36 +602,42 @@ func handleOAuth2SignIn(u *models.User, gothUser goth.User, ctx *context.Context // Instead, redirect them to the 2FA authentication page. _, err = models.GetTwoFactorByUID(u.ID) if err != nil { - if models.IsErrTwoFactorNotEnrolled(err) { - err = ctx.Session.Set("uid", u.ID) - if err != nil { - log.Error(fmt.Sprintf("Error setting session: %v", err)) - } - err = ctx.Session.Set("uname", u.Name) - if err != nil { - log.Error(fmt.Sprintf("Error setting session: %v", err)) - } - - // Clear whatever CSRF has right now, force to generate a new one - ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) - - // Register last login - u.SetLastLogin() - if err := models.UpdateUserCols(u, "last_login_unix"); err != nil { - ctx.ServerError("UpdateUserCols", err) - return - } - - if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 { - ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true) - ctx.RedirectToFirst(redirectTo) - return - } - - ctx.Redirect(setting.AppSubURL + "/") - } else { + if !models.IsErrTwoFactorNotEnrolled(err) { ctx.ServerError("UserSignIn", err) + return } + + err = ctx.Session.Set("uid", u.ID) + if err != nil { + log.Error(fmt.Sprintf("Error setting session: %v", err)) + } + err = ctx.Session.Set("uname", u.Name) + if err != nil { + log.Error(fmt.Sprintf("Error setting session: %v", err)) + } + + // Clear whatever CSRF has right now, force to generate a new one + ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) + + // Register last login + u.SetLastLogin() + if err := models.UpdateUserCols(u, "last_login_unix"); err != nil { + ctx.ServerError("UpdateUserCols", err) + return + } + + // update external user information + if err := models.UpdateExternalUser(u, gothUser); err != nil { + log.Error("UpdateExternalUser failed: %v", err) + } + + if redirectTo := ctx.GetCookie("redirect_to"); len(redirectTo) > 0 { + ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL, "", setting.SessionConfig.Secure, true) + ctx.RedirectToFirst(redirectTo) + return + } + + ctx.Redirect(setting.AppSubURL + "/") return } @@ -675,7 +682,7 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ } if hasUser { - return user, goth.User{}, nil + return user, gothUser, nil } // search in external linked users @@ -689,7 +696,7 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ } if hasUser { user, err = models.GetUserByID(externalLoginUser.UserID) - return user, goth.User{}, err + return user, gothUser, err } // no user found to login @@ -789,16 +796,18 @@ func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) { // Instead, redirect them to the 2FA authentication page. _, err = models.GetTwoFactorByUID(u.ID) if err != nil { - if models.IsErrTwoFactorNotEnrolled(err) { - err = models.LinkAccountToUser(u, gothUser.(goth.User)) - if err != nil { - ctx.ServerError("UserLinkAccount", err) - } else { - handleSignIn(ctx, u, signInForm.Remember) - } - } else { + if !models.IsErrTwoFactorNotEnrolled(err) { ctx.ServerError("UserLinkAccount", err) + return } + + err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User)) + if err != nil { + ctx.ServerError("UserLinkAccount", err) + return + } + + handleSignIn(ctx, u, signInForm.Remember) return } @@ -947,6 +956,11 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au } } + // update external user information + if err := models.UpdateExternalUser(u, gothUser.(goth.User)); err != nil { + log.Error("UpdateExternalUser failed: %v", err) + } + // Send confirmation email if setting.Service.RegisterEmailConfirm && u.ID > 1 { mailer.SendActivateAccountMail(ctx.Locale, u) diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go new file mode 100644 index 0000000000..800546f123 --- /dev/null +++ b/services/externalaccount/user.go @@ -0,0 +1,66 @@ +// 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 externalaccount + +import ( + "strconv" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/structs" + + "github.com/markbates/goth" +) + +// LinkAccountToUser link the gothUser to the user +func LinkAccountToUser(user *models.User, gothUser goth.User) error { + loginSource, err := models.GetActiveOAuth2LoginSourceByName(gothUser.Provider) + if err != nil { + return err + } + + externalLoginUser := &models.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, + } + + if err := models.LinkExternalToUser(user, externalLoginUser); err != nil { + return err + } + + externalID, err := strconv.ParseInt(externalLoginUser.ExternalID, 10, 64) + if err != nil { + return err + } + + var tp structs.GitServiceType + for _, s := range structs.SupportedFullGitService { + if strings.EqualFold(s.Name(), gothUser.Provider) { + tp = s + break + } + } + + if tp.Name() != "" { + return models.UpdateMigrationsByType(tp, externalID, user.ID) + } + + return nil +} From f9aba9ba0f07b77cb46dde6eda3c3f5b8fa841fe Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 14 Oct 2019 15:22:46 +0800 Subject: [PATCH 053/154] fix bug on FindExternalUsersByProvider (#8504) --- models/external_login_user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/external_login_user.go b/models/external_login_user.go index 5058fd1b4b..59c3732184 100644 --- a/models/external_login_user.go +++ b/models/external_login_user.go @@ -159,7 +159,7 @@ func FindExternalUsersByProvider(opts FindExternalUserOptions) ([]ExternalLoginU var users []ExternalLoginUser err := x.Where(opts.toConds()). Limit(opts.Limit, opts.Start). - Asc("id"). + OrderBy("login_source_id ASC, external_id ASC"). Find(&users) if err != nil { return nil, err From db657192d0349f7b10a62515fbf085d3a48d88f9 Mon Sep 17 00:00:00 2001 From: Maxim Tkachenko Date: Mon, 14 Oct 2019 22:24:26 +0700 Subject: [PATCH 054/154] Password Complexity Checks (#6230) Add password complexity checks. The default settings require a lowercase, uppercase, number and a special character within passwords. Co-Authored-By: T-M-A Co-Authored-By: Lanre Adelowo Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> Co-Authored-By: Lauris BH --- cmd/admin.go | 19 ++--- custom/conf/app.ini.sample | 5 +- .../doc/advanced/config-cheat-sheet.en-us.md | 5 ++ modules/password/password.go | 73 +++++++++++++++++ modules/setting/setting.go | 22 +++++ options/locale/locale_en-US.ini | 1 + routers/admin/users.go | 10 ++- routers/api/v1/admin/user.go | 14 +++- routers/user/auth.go | 11 +-- routers/user/setting/account.go | 3 + routers/user/setting/account_test.go | 81 ++++++++++++++----- 11 files changed, 207 insertions(+), 37 deletions(-) create mode 100644 modules/password/password.go 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/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index fd8d928ede..79d9960052 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -332,6 +332,9 @@ 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. +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 @@ -415,7 +418,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 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 b927793a50..100bb229ee 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -208,6 +208,11 @@ 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 ``][!"#$%&'()*+,./:;<=>?@\^_{|}~`-`` and space symbol. ## OpenID (`openid`) diff --git a/modules/password/password.go b/modules/password/password.go new file mode 100644 index 0000000000..54131b9641 --- /dev/null +++ b/modules/password/password.go @@ -0,0 +1,73 @@ +// 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" + "regexp" + "sync" + + "code.gitea.io/gitea/modules/setting" +) + +var matchComplexities = map[string]regexp.Regexp{} +var matchComplexityOnce sync.Once +var validChars string +var validComplexities = map[string]string{ + "lower": "abcdefghijklmnopqrstuvwxyz", + "upper": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "digit": "0123456789", + "spec": `][ !"#$%&'()*+,./:;<=>?@\^_{|}~` + "`-", +} + +// NewComplexity for preparation +func NewComplexity() { + matchComplexityOnce.Do(func() { + if len(setting.PasswordComplexity) > 0 { + for key, val := range setting.PasswordComplexity { + matchComplexity := regexp.MustCompile(val) + matchComplexities[key] = *matchComplexity + validChars += validComplexities[key] + } + } else { + for _, val := range validComplexities { + validChars += val + } + } + }) +} + +// IsComplexEnough return True if password is Complexity +func IsComplexEnough(pwd string) bool { + if len(setting.PasswordComplexity) > 0 { + NewComplexity() + for _, val := range matchComplexities { + if !val.MatchString(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/setting/setting.go b/modules/setting/setting.go index 8c61bdbb77..278ed4b107 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -146,6 +146,7 @@ var ( MinPasswordLength int ImportLocalPaths bool DisableGitHooks bool + PasswordComplexity map[string]string PasswordHashAlgo string // UI settings @@ -774,6 +775,27 @@ func NewContext() { InternalToken = loadInternalToken(sec) + var dictPC = map[string]string{ + "lower": "[a-z]+", + "upper": "[A-Z]+", + "digit": "[0-9]+", + "spec": `][ !"#$%&'()*+,./:;<=>?@\\^_{|}~` + "`-", + } + PasswordComplexity = make(map[string]string) + cfgdata := sec.Key("PASSWORD_COMPLEXITY").Strings(",") + for _, y := range cfgdata { + ts := strings.TrimSpace(y) + for a := range dictPC { + if strings.ToLower(ts) == a { + PasswordComplexity[ts] = dictPC[ts] + break + } + } + } + if len(PasswordComplexity) == 0 { + PasswordComplexity = dictPC + } + sec = Cfg.Section("attachment") AttachmentPath = sec.Key("PATH").MustString(path.Join(AppDataPath, "attachments")) if !filepath.IsAbs(AttachmentPath) { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e6c5839a64..4a92b08030 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -315,6 +315,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. diff --git a/routers/admin/users.go b/routers/admin/users.go index 660f116682..fdc4e0e371 100644 --- a/routers/admin/users.go +++ b/routers/admin/users.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/services/mailer" @@ -94,7 +95,10 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) { u.LoginName = form.LoginName } } - + if !password.IsComplexEnough(form.Password) { + ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplUserNew, &form) + return + } if err := models.CreateUser(u); err != nil { switch { case models.IsErrUserAlreadyExist(err): @@ -201,6 +205,10 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) { ctx.ServerError("UpdateUser", err) return } + if !password.IsComplexEnough(form.Password) { + ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplUserEdit, &form) + return + } u.HashPassword(form.Password) } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 70076b626b..f35ad297b0 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -6,9 +6,12 @@ package admin import ( + "errors" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/password" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/user" @@ -73,7 +76,11 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) { if ctx.Written() { return } - + if !password.IsComplexEnough(form.Password) { + err := errors.New("PasswordComplexity") + ctx.Error(400, "PasswordComplexity", err) + return + } if err := models.CreateUser(u); err != nil { if models.IsErrUserAlreadyExist(err) || models.IsErrEmailAlreadyUsed(err) || @@ -131,6 +138,11 @@ func EditUser(ctx *context.APIContext, form api.EditUserOption) { } if len(form.Password) > 0 { + if !password.IsComplexEnough(form.Password) { + err := errors.New("PasswordComplexity") + ctx.Error(400, "PasswordComplexity", err) + return + } var err error if u.Salt, err = models.GetUserSalt(); err != nil { ctx.Error(500, "UpdateUser", err) diff --git a/routers/user/auth.go b/routers/user/auth.go index 212d535a06..82a508e4dc 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/recaptcha" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -1334,6 +1335,11 @@ func ResetPasswdPost(ctx *context.Context) { ctx.Data["Err_Password"] = true ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil) return + } else if !password.IsComplexEnough(passwd) { + ctx.Data["IsResetForm"] = true + ctx.Data["Err_Password"] = true + ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplResetPassword, nil) + return } var err error @@ -1364,7 +1370,6 @@ func ResetPasswdPost(ctx *context.Context) { func MustChangePassword(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("auth.must_change_password") ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password" - ctx.HTML(200, tplMustChangePassword) } @@ -1372,16 +1377,12 @@ func MustChangePassword(ctx *context.Context) { // account was created by an admin func MustChangePasswordPost(ctx *context.Context, cpt *captcha.Captcha, form auth.MustChangePasswordForm) { ctx.Data["Title"] = ctx.Tr("auth.must_change_password") - ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password" - if ctx.HasError() { ctx.HTML(200, tplMustChangePassword) return } - u := ctx.User - // Make sure only requests for users who are eligible to change their password via // this method passes through if !u.MustChangePassword { diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go index 71d98fd3b9..c782224216 100644 --- a/routers/user/setting/account.go +++ b/routers/user/setting/account.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/password" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/services/mailer" @@ -52,6 +53,8 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { ctx.Flash.Error(ctx.Tr("settings.password_incorrect")) } else if form.Password != form.Retype { ctx.Flash.Error(ctx.Tr("form.password_not_match")) + } else if !password.IsComplexEnough(form.Password) { + ctx.Flash.Error(ctx.Tr("settings.password_complexity")) } else { var err error if ctx.User.Salt, err = models.GetUserSalt(); err != nil { diff --git a/routers/user/setting/account_test.go b/routers/user/setting/account_test.go index 59fbda1569..497ee658b0 100644 --- a/routers/user/setting/account_test.go +++ b/routers/user/setting/account_test.go @@ -19,36 +19,77 @@ import ( func TestChangePassword(t *testing.T) { oldPassword := "password" setting.MinPasswordLength = 6 + setting.PasswordComplexity = map[string]string{ + "lower": "[a-z]+", + "upper": "[A-Z]+", + "digit": "[0-9]+", + "spec": "[-_]+", + } + var pcLUN = map[string]string{ + "lower": "[a-z]+", + "upper": "[A-Z]+", + "digit": "[0-9]+", + } + var pcLU = map[string]string{ + "lower": "[a-z]+", + "upper": "[A-Z]+", + } for _, req := range []struct { - OldPassword string - NewPassword string - Retype string - Message string + OldPassword string + NewPassword string + Retype string + Message string + PasswordComplexity map[string]string }{ { - OldPassword: oldPassword, - NewPassword: "123456", - Retype: "123456", - Message: "", + OldPassword: oldPassword, + NewPassword: "Qwerty123456-", + Retype: "Qwerty123456-", + Message: "", + PasswordComplexity: setting.PasswordComplexity, }, { - OldPassword: oldPassword, - NewPassword: "12345", - Retype: "12345", - Message: "auth.password_too_short", + OldPassword: oldPassword, + NewPassword: "12345", + Retype: "12345", + Message: "auth.password_too_short", + PasswordComplexity: setting.PasswordComplexity, }, { - OldPassword: "12334", - NewPassword: "123456", - Retype: "123456", - Message: "settings.password_incorrect", + OldPassword: "12334", + NewPassword: "123456", + Retype: "123456", + Message: "settings.password_incorrect", + PasswordComplexity: setting.PasswordComplexity, }, { - OldPassword: oldPassword, - NewPassword: "123456", - Retype: "12345", - Message: "form.password_not_match", + OldPassword: oldPassword, + NewPassword: "123456", + Retype: "12345", + Message: "form.password_not_match", + PasswordComplexity: setting.PasswordComplexity, + }, + { + OldPassword: oldPassword, + NewPassword: "Qwerty", + Retype: "Qwerty", + Message: "settings.password_complexity", + PasswordComplexity: setting.PasswordComplexity, + }, + { + OldPassword: oldPassword, + NewPassword: "Qwerty", + Retype: "Qwerty", + Message: "settings.password_complexity", + PasswordComplexity: pcLUN, + }, + { + OldPassword: oldPassword, + NewPassword: "QWERTY", + Retype: "QWERTY", + Message: "settings.password_complexity", + PasswordComplexity: pcLU, }, } { models.PrepareTestEnv(t) From 8c8a93c02558dfe94fd5979fa6a9f1cad6bd6d21 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 14 Oct 2019 15:45:33 +0000 Subject: [PATCH 055/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_pl-PL.ini | 2 +- options/locale/locale_pt-BR.ini | 2 ++ options/locale/locale_zh-CN.ini | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 52cddf4735..17c6d6a0dd 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1797,7 +1797,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 f38380bb48..3c748fed84 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -632,6 +632,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 diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 7b62bf9c12..92b1771931 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -632,6 +632,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=派生自 From eb8975dcce5492a7a3af8d5b4b22de4145c4db1b Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Mon, 14 Oct 2019 14:43:48 -0300 Subject: [PATCH 056/154] Add nofollow to sign in links (#8509) --- templates/base/head_navbar.tmpl | 2 +- templates/repo/header.tmpl | 2 +- templates/user/auth/signin_navbar.tmpl | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 30316e7e5b..390a1fe804 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -125,7 +125,7 @@ {{.i18n.Tr "register"}} {{end}} - + {{.i18n.Tr "sign_in"}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 9fb3e32899..111609efef 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -36,7 +36,7 @@ {{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
- + {{$.i18n.Tr "repo.fork"}} diff --git a/templates/user/auth/signin_navbar.tmpl b/templates/user/auth/signin_navbar.tmpl index 3f0db26713..345e221296 100644 --- a/templates/user/auth/signin_navbar.tmpl +++ b/templates/user/auth/signin_navbar.tmpl @@ -1,9 +1,9 @@ {{if .EnableOpenIDSignIn}}
-
+
-
+
From db0d4ffdc7d0800b7785beddee4a715b1f7589bd Mon Sep 17 00:00:00 2001 From: 6543 <24977596+6543@users.noreply.github.com> Date: Mon, 14 Oct 2019 21:34:21 +0200 Subject: [PATCH 059/154] Changelog for 1.10.0-RC1 (#8510) * Changelog for 1.10.0 * clean up | remove TESTING and DOCS sction | short BUILD section Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> Co-Authored-By: John Olheiser <42128690+jolheiser@users.noreply.github.com> Co-Authored-By: zeripath --- CHANGELOG.md | 273 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 266 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c71af6b73..2a88eb1035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,265 @@ 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) @@ -122,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) @@ -736,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 From 30835226207f9a618bb2d4956fb209b1dbe099e6 Mon Sep 17 00:00:00 2001 From: Lauris BH Date: Tue, 15 Oct 2019 00:02:16 +0300 Subject: [PATCH 060/154] Starting v1.11.0 development From 086bfb8b4b655f46ac9471cbea9df70e912c315d Mon Sep 17 00:00:00 2001 From: jaqra <48099350+jaqra@users.noreply.github.com> Date: Tue, 15 Oct 2019 00:38:35 +0300 Subject: [PATCH 061/154] Add pagination to commit graph page (#8360) Fixes #8308 --- models/graph.go | 3 ++- models/graph_test.go | 2 +- routers/repo/commit.go | 6 ++++-- templates/repo/graph.tmpl | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) 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/routers/repo/commit.go b/routers/repo/commit.go index 3cedf70319..550e4c3a9c 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -91,7 +91,9 @@ func Graph(ctx *context.Context) { return } - graph, err := models.GetCommitGraph(ctx.Repo.GitRepo) + page := ctx.QueryInt("page") + + graph, err := models.GetCommitGraph(ctx.Repo.GitRepo, page) if err != nil { ctx.ServerError("GetCommitGraph", err) return @@ -103,8 +105,8 @@ func Graph(ctx *context.Context) { ctx.Data["CommitCount"] = commitsCount ctx.Data["Branch"] = ctx.Repo.BranchName ctx.Data["RequireGitGraph"] = true + ctx.Data["Page"] = context.NewPagination(int(commitsCount), setting.UI.GraphMaxCommitNum, page, 5) ctx.HTML(200, tplGraph) - } // SearchCommits render commits filtered by keyword diff --git a/templates/repo/graph.tmpl b/templates/repo/graph.tmpl index 2e8d0b5d91..20fe3d1527 100644 --- a/templates/repo/graph.tmpl +++ b/templates/repo/graph.tmpl @@ -37,4 +37,5 @@
+{{template "base/paginate" .}} {{template "base/footer" .}} From b6ef539ef46b728736764899a05be8236ca0c306 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 14 Oct 2019 21:39:15 +0000 Subject: [PATCH 062/154] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index c038444f62..6ea29efe76 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -314,6 +314,7 @@ 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. @@ -632,6 +633,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 @@ -839,6 +842,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 From 0be992a1e26f61a182113266a2eb34f77b87f9b4 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 15 Oct 2019 06:05:57 +0800 Subject: [PATCH 063/154] Make static resouces web browser cache time customized on app.ini (#8442) * make static resouces web browser cache time customized on app.ini * Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: zeripath * Update custom/conf/app.ini.sample Co-Authored-By: Antoine GIRARD * fix docs --- custom/conf/app.ini.sample | 2 ++ docs/content/doc/advanced/config-cheat-sheet.en-us.md | 1 + docs/content/doc/advanced/config-cheat-sheet.zh-cn.md | 1 + modules/setting/setting.go | 2 ++ routers/routes/routes.go | 8 ++++---- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 79d9960052..442ac4b5bb 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -243,6 +243,8 @@ 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 +; 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] 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 100bb229ee..6313e705d4 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -140,6 +140,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. 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 ab73e2059e..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`。 diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 278ed4b107..629a89766f 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 @@ -607,6 +608,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) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 8dfcdb9c9b..0db0af43f0 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -139,14 +139,14 @@ func NewMacaron() *macaron.Macaron { m.Use(public.Custom( &public.Options{ SkipLogging: setting.DisableRouterLog, - ExpiresAfter: time.Hour * 6, + ExpiresAfter: setting.StaticCacheTime, }, )) m.Use(public.Static( &public.Options{ Directory: path.Join(setting.StaticRootPath, "public"), SkipLogging: setting.DisableRouterLog, - ExpiresAfter: time.Hour * 6, + ExpiresAfter: setting.StaticCacheTime, }, )) m.Use(public.StaticHandler( @@ -154,7 +154,7 @@ func NewMacaron() *macaron.Macaron { &public.Options{ Prefix: "avatars", SkipLogging: setting.DisableRouterLog, - ExpiresAfter: time.Hour * 6, + ExpiresAfter: setting.StaticCacheTime, }, )) m.Use(public.StaticHandler( @@ -162,7 +162,7 @@ func NewMacaron() *macaron.Macaron { &public.Options{ Prefix: "repo-avatars", SkipLogging: setting.DisableRouterLog, - ExpiresAfter: time.Hour * 6, + ExpiresAfter: setting.StaticCacheTime, }, )) From 733c898a907b23fa9e0c1bf108be5c5d9f9f7eb0 Mon Sep 17 00:00:00 2001 From: 6543 <24977596+6543@users.noreply.github.com> Date: Tue, 15 Oct 2019 00:40:17 +0200 Subject: [PATCH 064/154] [Branch View] Add Included TAG (#8449) * included message * add property IsIncluded * Add Orange Lable --- options/locale/locale_en-US.ini | 2 ++ routers/repo/branch.go | 4 ++++ templates/repo/branch/list.tmpl | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4a92b08030..b5a3d8c592 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1453,6 +1453,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 diff --git a/routers/repo/branch.go b/routers/repo/branch.go index 5d78518491..0c06de3ea6 100644 --- a/routers/repo/branch.go +++ b/routers/repo/branch.go @@ -28,6 +28,7 @@ type Branch struct { Commit *git.Commit IsProtected bool IsDeleted bool + IsIncluded bool DeletedBranch *models.DeletedBranch CommitsAhead int CommitsBehind int @@ -203,10 +204,13 @@ func loadBranches(ctx *context.Context) []*Branch { } } + isIncluded := divergence.Ahead == 0 && ctx.Repo.Repository.DefaultBranch != branchName + branches[i] = &Branch{ Name: branchName, Commit: commit, IsProtected: isProtected, + IsIncluded: isIncluded, CommitsAhead: divergence.Ahead, CommitsBehind: divergence.Behind, LatestPullRequest: pr, diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 9c53f4e67a..26493298da 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -75,7 +75,11 @@ {{if not .LatestPullRequest}} - {{if and (not .IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} + {{if .IsIncluded}} + + {{$.i18n.Tr "repo.branch.included"}} + + {{else if and (not .IsDeleted) $.AllowsPulls (gt .CommitsAhead 0)}} From 8ad26976114c4fed6269a40e52632d065167bd20 Mon Sep 17 00:00:00 2001 From: David Svantesson Date: Tue, 15 Oct 2019 02:55:21 +0200 Subject: [PATCH 065/154] Recalculate repository access only for specific user (#8481) * Recalculate repository access only for specific user Signed-off-by: David Svantesson * Handle user repositories as well, and only add access if minimum mode * Need to get repo owner to check if organization --- models/access.go | 49 ++++++++++++++++++++++++++++++++++++ models/org_team.go | 4 +-- models/repo_collaboration.go | 19 +++++++++----- 3 files changed, 64 insertions(+), 8 deletions(-) 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/org_team.go b/models/org_team.go index 9170ea2c2a..10d53e3a86 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -723,7 +723,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 { @@ -768,7 +768,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/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}) } From cea8ea5ae64bbb287ed7011c6fc2e51ccdfb9cb3 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Mon, 14 Oct 2019 22:31:09 -0300 Subject: [PATCH 066/154] Support inline rendering of CUSTOM_URL_SCHEMES (#8496) * Support inline rendering of CUSTOM_URL_SCHEMES * Fix lint * Add tests * Fix lint --- modules/markup/html.go | 26 ++++++++++++++++++++++++++ modules/markup/html_test.go | 19 +++++++++++++++++++ modules/markup/markup.go | 4 ++++ modules/markup/sanitizer.go | 32 +++++++++++++++++++------------- 4 files changed, 68 insertions(+), 13 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index fc823b1f30..1ff7a41cbb 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -92,6 +92,32 @@ func getIssueFullPattern() *regexp.Regexp { return issueFullPattern } +// 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) + } + linkRegex, _ = xurls.StrictMatchingScheme(strings.Join(withAuth, "|")) +} + // IsSameDomain checks if given url string has the same hostname as current Gitea instance func IsSameDomain(s string) bool { if strings.HasPrefix(s, "/") { 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/sanitizer.go b/modules/markup/sanitizer.go index fd6f90b2ab..f873e8105e 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -28,22 +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...) - - // Allow keyword markup - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^` + keywordClass + `$`)).OnElements("span") + 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() From ebe8ff782fd56378e9946b716fd7e95babf67411 Mon Sep 17 00:00:00 2001 From: Benson Muite Date: Tue, 15 Oct 2019 05:39:55 +0300 Subject: [PATCH 067/154] Update config-cheat-sheet.en-us.md (#8497) * Update config-cheat-sheet.en-us.md Add more information on configuring URI hyperlink rendering for Markdown. * Update config-cheat-sheet.en-us.md Update description as suggested by @guillep2k * Update docs/content/doc/advanced/config-cheat-sheet.en-us.md Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> --- docs/content/doc/advanced/config-cheat-sheet.en-us.md | 3 +++ 1 file changed, 3 insertions(+) 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 6313e705d4..29a971da12 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -108,6 +108,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`) From 34fb9d68a5567423ddde736ff42f9780f4048366 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 15 Oct 2019 11:28:40 +0800 Subject: [PATCH 068/154] Move AddTestPullRequestTask to pull service package from models (#8324) * move AddTestPullRequestTask to pull service package from models * fix fmt --- models/pull.go | 87 ------------------------------------ modules/repofiles/update.go | 3 +- routers/repo/pull.go | 2 +- services/pull/merge.go | 2 +- services/pull/pull.go | 88 +++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 90 deletions(-) diff --git a/models/pull.go b/models/pull.go index ff1f7b773b..962e433fb0 100644 --- a/models/pull.go +++ b/models/pull.go @@ -1072,93 +1072,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{ diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index 1a1fe6c389..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" @@ -498,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/routers/repo/pull.go b/routers/repo/pull.go index 7af01c46ba..8b97e55670 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -820,7 +820,7 @@ func TriggerTask(ctx *context.Context) { log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) go models.HookQueue.Add(repo.ID) - go models.AddTestPullRequestTask(pusher, repo.ID, branch, true) + go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true) ctx.Status(202) } diff --git a/services/pull/merge.go b/services/pull/merge.go index e83784f31e..355d6dd911 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -50,7 +50,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor } defer func() { - go models.AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false) + go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false) }() // Clone base repo. diff --git a/services/pull/pull.go b/services/pull/pull.go index 0dbd8fcd1a..3c584fce74 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -8,6 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" ) @@ -47,3 +48,90 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6 return nil } + +func checkForInvalidation(requests models.PullRequestList, repoID int64, doer *models.User, branch string) error { + repo, err := models.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 +} + +func addHeadRepoTasks(prs []*models.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 *models.User, repoID int64, branch string, isSync bool) { + log.Trace("AddTestPullRequestTask [head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch) + prs, err := models.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 := models.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 = models.PrepareWebhooks(pr.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + Action: api.HookIssueSynchronized, + Index: pr.Issue.Index, + PullRequest: pr.Issue.PullRequest.APIFormat(), + Repository: pr.Issue.Repo.APIFormat(models.AccessModeNone), + Sender: doer.APIFormat(), + }); err != nil { + log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err) + continue + } + go models.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 = models.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() + } +} From 20477a69ea123a7800ebf94bfd2225eb9ae90e8f Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Tue, 15 Oct 2019 13:03:05 +0800 Subject: [PATCH 069/154] Move clearlabels from models to issue service (#8326) * move clearlabels from models to issue service * improve code * Apply suggestions from code review Co-Authored-By: zeripath --- models/issue.go | 34 ------------- modules/notification/notification.go | 2 + modules/notification/webhook/webhook.go | 67 +++++++++++++++++++++++++ routers/api/v1/repo/issue_label.go | 3 +- routers/repo/issue_label.go | 3 +- services/issue/label.go | 21 ++++++++ 6 files changed, 94 insertions(+), 36 deletions(-) create mode 100644 modules/notification/webhook/webhook.go create mode 100644 services/issue/label.go diff --git a/models/issue.go b/models/issue.go index fc675a3ffb..90925f92f5 100644 --- a/models/issue.go +++ b/models/issue.go @@ -596,40 +596,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 } diff --git a/modules/notification/notification.go b/modules/notification/notification.go index e0de88346d..06220ecb04 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -11,6 +11,7 @@ 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" ) var ( @@ -27,6 +28,7 @@ func init() { RegisterNotifier(ui.NewNotifier()) RegisterNotifier(mail.NewNotifier()) RegisterNotifier(indexer.NewNotifier()) + RegisterNotifier(webhook.NewNotifier()) } // NotifyCreateIssueComment notifies issue comment related message to notifiers 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/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index e66bc35e90..7bd76c24c2 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" api "code.gitea.io/gitea/modules/structs" + issue_service "code.gitea.io/gitea/services/issue" ) // ListIssueLabels list all the labels of an issue @@ -314,7 +315,7 @@ func ClearIssueLabels(ctx *context.APIContext) { return } - if err := issue.ClearLabels(ctx.User); err != nil { + if err := issue_service.ClearLabels(issue, ctx.User); err != nil { ctx.Error(500, "ClearLabels", err) return } diff --git a/routers/repo/issue_label.go b/routers/repo/issue_label.go index 452a0a4c0c..53c37e2e97 100644 --- a/routers/repo/issue_label.go +++ b/routers/repo/issue_label.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" + issue_service "code.gitea.io/gitea/services/issue" ) const ( @@ -132,7 +133,7 @@ func UpdateIssueLabel(ctx *context.Context) { switch action := ctx.Query("action"); action { case "clear": for _, issue := range issues { - if err := issue.ClearLabels(ctx.User); err != nil { + if err := issue_service.ClearLabels(issue, ctx.User); err != nil { ctx.ServerError("ClearLabels", err) return } diff --git a/services/issue/label.go b/services/issue/label.go new file mode 100644 index 0000000000..4af8c2b97e --- /dev/null +++ b/services/issue/label.go @@ -0,0 +1,21 @@ +// 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 issue + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/notification" +) + +// ClearLabels clears all of an issue's labels +func ClearLabels(issue *models.Issue, doer *models.User) (err error) { + if err = issue.ClearLabels(doer); err != nil { + return + } + + notification.NotifyIssueClearLabels(doer, issue) + + return nil +} From 6fa14ac3c82433f7f9bff4f4fe2ec6e33deb82ec Mon Sep 17 00:00:00 2001 From: Benson Muite Date: Tue, 15 Oct 2019 10:14:58 +0300 Subject: [PATCH 070/154] Update app.ini.sample (#8498) * Update app.ini.sample Give further information on hyperlink rendering in Markdown * Update app.ini.sample Follow feedback from @guillep2k for CUSTOM_URL in markdown to indicate http and https are always rendered as links. * Update custom/conf/app.ini.sample Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Update custom/conf/app.ini.sample Co-Authored-By: Antoine GIRARD --- custom/conf/app.ini.sample | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 442ac4b5bb..5ee2162ba3 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -141,8 +141,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 @@ -826,4 +827,4 @@ QUEUE_TYPE = channel QUEUE_LENGTH = 1000 ; Task queue connction 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" \ No newline at end of file +QUEUE_CONN_STR = "addrs=127.0.0.1:6379 db=0" From 1e9b33052553bb546ca526f04816579e2853c8da Mon Sep 17 00:00:00 2001 From: "oscar.lofwenhamn" <44643697+oscarlofwenhamn@users.noreply.github.com> Date: Tue, 15 Oct 2019 10:40:42 +0200 Subject: [PATCH 071/154] Update CodeMirror to version 5.49.0 (#8381) * Update CodeMirror to version 5.49.0 * Update CodeMirror versions in librejs and VERSIONS --- public/vendor/VERSIONS | 2 +- public/vendor/librejs.html | 4 +- .../codemirror/addon/comment/comment.js | 209 ++++ .../addon/comment/continuecomment.js | 78 ++ .../codemirror/addon/dialog/dialog.css | 32 + .../plugins/codemirror/addon/dialog/dialog.js | 161 +++ .../codemirror/addon/display/autorefresh.js | 47 + .../codemirror/addon/display/fullscreen.css | 6 + .../codemirror/addon/display/fullscreen.js | 41 + .../plugins/codemirror/addon/display/panel.js | 127 +++ .../codemirror/addon/display/placeholder.js | 63 ++ .../codemirror/addon/display/rulers.js | 51 + .../codemirror/addon/edit/closebrackets.js | 191 ++++ .../plugins/codemirror/addon/edit/closetag.js | 184 +++ .../codemirror/addon/edit/continuelist.js | 99 ++ .../codemirror/addon/edit/matchbrackets.js | 150 +++ .../codemirror/addon/edit/matchtags.js | 66 ++ .../codemirror/addon/edit/trailingspace.js | 27 + .../codemirror/addon/fold/brace-fold.js | 105 ++ .../codemirror/addon/fold/comment-fold.js | 59 + .../plugins/codemirror/addon/fold/foldcode.js | 152 +++ .../codemirror/addon/fold/foldgutter.css | 20 + .../codemirror/addon/fold/foldgutter.js | 151 +++ .../codemirror/addon/fold/indent-fold.js | 48 + .../codemirror/addon/fold/markdown-fold.js | 49 + .../plugins/codemirror/addon/fold/xml-fold.js | 184 +++ .../codemirror/addon/hint/anyword-hint.js | 41 + .../plugins/codemirror/addon/hint/css-hint.js | 60 + .../codemirror/addon/hint/html-hint.js | 350 ++++++ .../codemirror/addon/hint/javascript-hint.js | 157 +++ .../codemirror/addon/hint/show-hint.css | 36 + .../codemirror/addon/hint/show-hint.js | 460 ++++++++ .../plugins/codemirror/addon/hint/sql-hint.js | 304 +++++ .../plugins/codemirror/addon/hint/xml-hint.js | 123 ++ .../addon/lint/coffeescript-lint.js | 47 + .../plugins/codemirror/addon/lint/css-lint.js | 40 + .../codemirror/addon/lint/html-lint.js | 59 + .../codemirror/addon/lint/javascript-lint.js | 63 ++ .../codemirror/addon/lint/json-lint.js | 40 + .../plugins/codemirror/addon/lint/lint.css | 73 ++ .../plugins/codemirror/addon/lint/lint.js | 252 +++++ .../codemirror/addon/lint/yaml-lint.js | 41 + .../plugins/codemirror/addon/merge/merge.css | 119 ++ .../plugins/codemirror/addon/merge/merge.js | 1002 +++++++++++++++++ .../plugins/codemirror/addon/mode/loadmode.js | 2 +- .../codemirror/addon/mode/multiplex.js | 18 +- .../codemirror/addon/mode/multiplex_test.js | 2 +- .../plugins/codemirror/addon/mode/overlay.js | 15 +- .../plugins/codemirror/addon/mode/simple.js | 15 +- .../codemirror/addon/runmode/colorize.js | 40 + .../addon/runmode/runmode-standalone.js | 158 +++ .../codemirror/addon/runmode/runmode.js | 72 ++ .../codemirror/addon/runmode/runmode.node.js | 197 ++++ .../addon/scroll/annotatescrollbar.js | 122 ++ .../codemirror/addon/scroll/scrollpastend.js | 48 + .../addon/scroll/simplescrollbars.css | 66 ++ .../addon/scroll/simplescrollbars.js | 152 +++ .../codemirror/addon/search/jump-to-line.js | 50 + .../addon/search/match-highlighter.js | 165 +++ .../addon/search/matchesonscrollbar.css | 8 + .../addon/search/matchesonscrollbar.js | 97 ++ .../plugins/codemirror/addon/search/search.js | 260 +++++ .../codemirror/addon/search/searchcursor.js | 293 +++++ .../codemirror/addon/selection/active-line.js | 72 ++ .../addon/selection/mark-selection.js | 119 ++ .../addon/selection/selection-pointer.js | 98 ++ .../plugins/codemirror/addon/tern/tern.css | 87 ++ .../plugins/codemirror/addon/tern/tern.js | 718 ++++++++++++ .../plugins/codemirror/addon/tern/worker.js | 44 + .../plugins/codemirror/addon/wrap/hardwrap.js | 145 +++ .../vendor/plugins/codemirror/mode/apl/apl.js | 2 +- .../plugins/codemirror/mode/apl/index.html | 2 +- .../codemirror/mode/asciiarmor/asciiarmor.js | 3 +- .../codemirror/mode/asciiarmor/index.html | 4 +- .../plugins/codemirror/mode/asn.1/asn.1.js | 2 +- .../plugins/codemirror/mode/asn.1/index.html | 5 +- .../codemirror/mode/asterisk/asterisk.js | 30 +- .../codemirror/mode/asterisk/index.html | 5 +- .../codemirror/mode/brainfuck/brainfuck.js | 2 +- .../codemirror/mode/brainfuck/index.html | 2 +- .../plugins/codemirror/mode/clike/clike.js | 235 ++-- .../plugins/codemirror/mode/clike/index.html | 36 +- .../plugins/codemirror/mode/clike/scala.html | 2 +- .../plugins/codemirror/mode/clike/test.js | 124 +- .../codemirror/mode/clojure/clojure.js | 526 +++++---- .../codemirror/mode/clojure/index.html | 92 +- .../plugins/codemirror/mode/clojure/test.js | 384 +++++++ .../plugins/codemirror/mode/cmake/cmake.js | 2 +- .../plugins/codemirror/mode/cmake/index.html | 2 +- .../plugins/codemirror/mode/cobol/cobol.js | 2 +- .../plugins/codemirror/mode/cobol/index.html | 2 +- .../mode/coffeescript/coffeescript.js | 6 +- .../codemirror/mode/coffeescript/index.html | 4 +- .../codemirror/mode/commonlisp/commonlisp.js | 5 +- .../codemirror/mode/commonlisp/index.html | 2 +- .../codemirror/mode/crystal/crystal.js | 90 +- .../codemirror/mode/crystal/index.html | 19 +- .../vendor/plugins/codemirror/mode/css/css.js | 76 +- .../plugins/codemirror/mode/css/gss.html | 5 +- .../plugins/codemirror/mode/css/gss_test.js | 2 +- .../plugins/codemirror/mode/css/index.html | 4 +- .../plugins/codemirror/mode/css/less.html | 6 +- .../plugins/codemirror/mode/css/less_test.js | 10 +- .../plugins/codemirror/mode/css/scss.html | 3 +- .../plugins/codemirror/mode/css/scss_test.js | 12 +- .../plugins/codemirror/mode/css/test.js | 59 +- .../plugins/codemirror/mode/cypher/cypher.js | 12 +- .../plugins/codemirror/mode/cypher/index.html | 5 +- .../plugins/codemirror/mode/cypher/test.js | 37 + public/vendor/plugins/codemirror/mode/d/d.js | 11 +- .../plugins/codemirror/mode/d/index.html | 2 +- .../vendor/plugins/codemirror/mode/d/test.js | 11 + .../plugins/codemirror/mode/dart/dart.js | 15 +- .../plugins/codemirror/mode/dart/index.html | 2 +- .../plugins/codemirror/mode/diff/diff.js | 2 +- .../plugins/codemirror/mode/diff/index.html | 2 +- .../plugins/codemirror/mode/django/django.js | 2 +- .../plugins/codemirror/mode/django/index.html | 4 +- .../codemirror/mode/dockerfile/dockerfile.js | 184 ++- .../codemirror/mode/dockerfile/index.html | 4 +- .../codemirror/mode/dockerfile/test.js | 128 +++ .../vendor/plugins/codemirror/mode/dtd/dtd.js | 2 +- .../plugins/codemirror/mode/dtd/index.html | 4 +- .../plugins/codemirror/mode/dylan/dylan.js | 20 +- .../plugins/codemirror/mode/dylan/index.html | 4 +- .../plugins/codemirror/mode/dylan/test.js | 2 +- .../plugins/codemirror/mode/ebnf/ebnf.js | 2 +- .../plugins/codemirror/mode/ebnf/index.html | 4 +- .../vendor/plugins/codemirror/mode/ecl/ecl.js | 2 +- .../plugins/codemirror/mode/ecl/index.html | 2 +- .../plugins/codemirror/mode/eiffel/eiffel.js | 2 +- .../plugins/codemirror/mode/eiffel/index.html | 2 +- .../vendor/plugins/codemirror/mode/elm/elm.js | 4 +- .../plugins/codemirror/mode/elm/index.html | 4 +- .../plugins/codemirror/mode/erlang/erlang.js | 7 +- .../plugins/codemirror/mode/erlang/index.html | 4 +- .../plugins/codemirror/mode/factor/factor.js | 52 +- .../plugins/codemirror/mode/factor/index.html | 2 +- .../vendor/plugins/codemirror/mode/fcl/fcl.js | 2 +- .../plugins/codemirror/mode/fcl/index.html | 2 +- .../plugins/codemirror/mode/forth/forth.js | 2 +- .../plugins/codemirror/mode/forth/index.html | 2 +- .../codemirror/mode/fortran/fortran.js | 2 +- .../codemirror/mode/fortran/index.html | 4 +- .../vendor/plugins/codemirror/mode/gas/gas.js | 2 +- .../plugins/codemirror/mode/gas/index.html | 2 +- .../vendor/plugins/codemirror/mode/gfm/gfm.js | 9 +- .../plugins/codemirror/mode/gfm/index.html | 51 +- .../plugins/codemirror/mode/gfm/test.js | 94 +- .../codemirror/mode/gherkin/gherkin.js | 2 +- .../codemirror/mode/gherkin/index.html | 2 +- .../vendor/plugins/codemirror/mode/go/go.js | 12 +- .../plugins/codemirror/mode/go/index.html | 2 +- .../plugins/codemirror/mode/groovy/groovy.js | 13 +- .../plugins/codemirror/mode/groovy/index.html | 2 +- .../plugins/codemirror/mode/haml/haml.js | 2 +- .../plugins/codemirror/mode/haml/index.html | 2 +- .../plugins/codemirror/mode/haml/test.js | 2 +- .../codemirror/mode/handlebars/handlebars.js | 12 +- .../codemirror/mode/handlebars/index.html | 5 +- .../mode/haskell-literate/haskell-literate.js | 2 +- .../mode/haskell-literate/index.html | 2 +- .../codemirror/mode/haskell/haskell.js | 21 +- .../codemirror/mode/haskell/index.html | 4 +- .../plugins/codemirror/mode/haxe/haxe.js | 4 +- .../plugins/codemirror/mode/haxe/index.html | 4 +- .../mode/htmlembedded/htmlembedded.js | 11 +- .../codemirror/mode/htmlembedded/index.html | 4 +- .../codemirror/mode/htmlmixed/htmlmixed.js | 14 +- .../codemirror/mode/htmlmixed/index.html | 33 +- .../plugins/codemirror/mode/http/http.js | 2 +- .../plugins/codemirror/mode/http/index.html | 4 +- .../vendor/plugins/codemirror/mode/idl/idl.js | 2 +- .../plugins/codemirror/mode/idl/index.html | 5 +- .../vendor/plugins/codemirror/mode/index.html | 9 +- .../codemirror/mode/javascript/index.html | 4 +- .../codemirror/mode/javascript/javascript.js | 511 ++++++--- .../codemirror/mode/javascript/json-ld.html | 4 +- .../codemirror/mode/javascript/test.js | 310 ++++- .../mode/javascript/typescript.html | 21 +- .../plugins/codemirror/mode/jinja2/index.html | 4 +- .../plugins/codemirror/mode/jinja2/jinja2.js | 10 +- .../plugins/codemirror/mode/jsx/index.html | 6 +- .../vendor/plugins/codemirror/mode/jsx/jsx.js | 9 +- .../plugins/codemirror/mode/jsx/test.js | 24 +- .../plugins/codemirror/mode/julia/index.html | 5 +- .../plugins/codemirror/mode/julia/julia.js | 311 ++--- .../codemirror/mode/livescript/index.html | 2 +- .../codemirror/mode/livescript/livescript.js | 4 +- .../plugins/codemirror/mode/lua/index.html | 4 +- .../vendor/plugins/codemirror/mode/lua/lua.js | 2 +- .../codemirror/mode/markdown/index.html | 94 +- .../codemirror/mode/markdown/markdown.js | 379 ++++--- .../plugins/codemirror/mode/markdown/test.js | 427 ++++++- .../codemirror/mode/mathematica/index.html | 2 +- .../mode/mathematica/mathematica.js | 6 +- .../plugins/codemirror/mode/mbox/index.html | 2 +- .../plugins/codemirror/mode/mbox/mbox.js | 2 +- public/vendor/plugins/codemirror/mode/meta.js | 50 +- .../plugins/codemirror/mode/mirc/index.html | 3 +- .../plugins/codemirror/mode/mirc/mirc.js | 4 +- .../plugins/codemirror/mode/mllike/index.html | 21 +- .../plugins/codemirror/mode/mllike/mllike.js | 248 +++- .../codemirror/mode/modelica/index.html | 2 +- .../codemirror/mode/modelica/modelica.js | 2 +- .../plugins/codemirror/mode/mscgen/index.html | 4 +- .../plugins/codemirror/mode/mscgen/mscgen.js | 16 +- .../codemirror/mode/mscgen/mscgen_test.js | 13 +- .../codemirror/mode/mscgen/msgenny_test.js | 10 +- .../plugins/codemirror/mode/mscgen/xu_test.js | 28 +- .../plugins/codemirror/mode/mumps/index.html | 4 +- .../plugins/codemirror/mode/mumps/mumps.js | 2 +- .../plugins/codemirror/mode/nginx/index.html | 6 +- .../plugins/codemirror/mode/nginx/nginx.js | 2 +- .../plugins/codemirror/mode/nsis/index.html | 2 +- .../plugins/codemirror/mode/nsis/nsis.js | 24 +- .../codemirror/mode/ntriples/index.html | 37 +- .../codemirror/mode/ntriples/ntriples.js | 17 +- .../plugins/codemirror/mode/octave/index.html | 5 +- .../plugins/codemirror/mode/octave/octave.js | 14 +- .../plugins/codemirror/mode/oz/index.html | 6 +- .../vendor/plugins/codemirror/mode/oz/oz.js | 4 +- .../plugins/codemirror/mode/pascal/index.html | 4 +- .../plugins/codemirror/mode/pascal/pascal.js | 20 +- .../plugins/codemirror/mode/pegjs/index.html | 4 +- .../plugins/codemirror/mode/pegjs/pegjs.js | 2 +- .../plugins/codemirror/mode/perl/index.html | 4 +- .../plugins/codemirror/mode/perl/perl.js | 2 +- .../plugins/codemirror/mode/php/index.html | 4 +- .../vendor/plugins/codemirror/mode/php/php.js | 12 +- .../plugins/codemirror/mode/php/test.js | 2 +- .../plugins/codemirror/mode/pig/index.html | 2 +- .../vendor/plugins/codemirror/mode/pig/pig.js | 2 +- .../codemirror/mode/powershell/index.html | 3 +- .../codemirror/mode/powershell/powershell.js | 8 +- .../codemirror/mode/powershell/test.js | 12 +- .../codemirror/mode/properties/index.html | 2 +- .../codemirror/mode/properties/properties.js | 2 +- .../codemirror/mode/protobuf/index.html | 44 +- .../codemirror/mode/protobuf/protobuf.js | 5 +- .../codemirror/mode/{jade => pug}/index.html | 20 +- .../mode/{jade/jade.js => pug/pug.js} | 7 +- .../plugins/codemirror/mode/puppet/index.html | 2 +- .../plugins/codemirror/mode/puppet/puppet.js | 2 +- .../plugins/codemirror/mode/python/index.html | 15 +- .../plugins/codemirror/mode/python/python.js | 155 ++- .../plugins/codemirror/mode/python/test.js | 18 +- .../plugins/codemirror/mode/q/index.html | 4 +- public/vendor/plugins/codemirror/mode/q/q.js | 16 +- .../plugins/codemirror/mode/r/index.html | 77 +- public/vendor/plugins/codemirror/mode/r/r.js | 64 +- .../codemirror/mode/rpm/changes/index.html | 4 +- .../plugins/codemirror/mode/rpm/index.html | 4 +- .../vendor/plugins/codemirror/mode/rpm/rpm.js | 2 +- .../plugins/codemirror/mode/rst/index.html | 4 +- .../vendor/plugins/codemirror/mode/rst/rst.js | 2 +- .../plugins/codemirror/mode/ruby/index.html | 2 +- .../plugins/codemirror/mode/ruby/ruby.js | 67 +- .../plugins/codemirror/mode/ruby/test.js | 11 +- .../plugins/codemirror/mode/rust/index.html | 4 +- .../plugins/codemirror/mode/rust/rust.js | 3 +- .../plugins/codemirror/mode/rust/test.js | 2 +- .../plugins/codemirror/mode/sas/index.html | 4 +- .../vendor/plugins/codemirror/mode/sas/sas.js | 98 +- .../plugins/codemirror/mode/sass/index.html | 6 +- .../plugins/codemirror/mode/sass/sass.js | 98 +- .../plugins/codemirror/mode/sass/test.js | 122 ++ .../plugins/codemirror/mode/scheme/index.html | 2 +- .../plugins/codemirror/mode/scheme/scheme.js | 28 +- .../plugins/codemirror/mode/shell/index.html | 4 +- .../plugins/codemirror/mode/shell/shell.js | 83 +- .../plugins/codemirror/mode/shell/test.js | 17 +- .../plugins/codemirror/mode/sieve/index.html | 2 +- .../plugins/codemirror/mode/sieve/sieve.js | 4 +- .../plugins/codemirror/mode/slim/index.html | 2 +- .../plugins/codemirror/mode/slim/slim.js | 2 +- .../plugins/codemirror/mode/slim/test.js | 2 +- .../codemirror/mode/smalltalk/index.html | 2 +- .../codemirror/mode/smalltalk/smalltalk.js | 2 +- .../plugins/codemirror/mode/smarty/index.html | 4 +- .../plugins/codemirror/mode/smarty/smarty.js | 6 +- .../plugins/codemirror/mode/solr/index.html | 4 +- .../plugins/codemirror/mode/solr/solr.js | 6 +- .../plugins/codemirror/mode/soy/index.html | 4 +- .../vendor/plugins/codemirror/mode/soy/soy.js | 356 +++++- .../plugins/codemirror/mode/soy/test.js | 204 ++++ .../plugins/codemirror/mode/sparql/index.html | 2 +- .../plugins/codemirror/mode/sparql/sparql.js | 4 +- .../codemirror/mode/spreadsheet/index.html | 4 +- .../mode/spreadsheet/spreadsheet.js | 2 +- .../plugins/codemirror/mode/sql/index.html | 13 +- .../vendor/plugins/codemirror/mode/sql/sql.js | 193 +++- .../plugins/codemirror/mode/stex/index.html | 8 +- .../plugins/codemirror/mode/stex/stex.js | 21 +- .../plugins/codemirror/mode/stex/test.js | 11 +- .../plugins/codemirror/mode/stylus/index.html | 2 +- .../plugins/codemirror/mode/stylus/stylus.js | 18 +- .../plugins/codemirror/mode/swift/index.html | 70 +- .../plugins/codemirror/mode/swift/swift.js | 117 +- .../plugins/codemirror/mode/swift/test.js | 162 +++ .../plugins/codemirror/mode/tcl/index.html | 2 +- .../vendor/plugins/codemirror/mode/tcl/tcl.js | 2 +- .../codemirror/mode/textile/index.html | 2 +- .../plugins/codemirror/mode/textile/test.js | 6 +- .../codemirror/mode/textile/textile.js | 4 +- .../codemirror/mode/tiddlywiki/index.html | 4 +- .../codemirror/mode/tiddlywiki/tiddlywiki.js | 4 +- .../plugins/codemirror/mode/tiki/index.html | 6 +- .../plugins/codemirror/mode/tiki/tiki.js | 18 +- .../plugins/codemirror/mode/toml/index.html | 4 +- .../plugins/codemirror/mode/toml/toml.js | 2 +- .../codemirror/mode/tornado/index.html | 4 +- .../codemirror/mode/tornado/tornado.js | 2 +- .../plugins/codemirror/mode/troff/index.html | 2 +- .../plugins/codemirror/mode/troff/troff.js | 2 +- .../codemirror/mode/ttcn-cfg/index.html | 5 +- .../codemirror/mode/ttcn-cfg/ttcn-cfg.js | 2 +- .../plugins/codemirror/mode/ttcn/index.html | 5 +- .../plugins/codemirror/mode/ttcn/ttcn.js | 2 +- .../plugins/codemirror/mode/turtle/index.html | 3 +- .../plugins/codemirror/mode/turtle/turtle.js | 2 +- .../plugins/codemirror/mode/twig/index.html | 4 +- .../plugins/codemirror/mode/twig/twig.js | 4 +- .../plugins/codemirror/mode/vb/index.html | 85 +- .../vendor/plugins/codemirror/mode/vb/vb.js | 17 +- .../codemirror/mode/vbscript/index.html | 4 +- .../codemirror/mode/vbscript/vbscript.js | 2 +- .../codemirror/mode/velocity/index.html | 2 +- .../codemirror/mode/velocity/velocity.js | 4 +- .../codemirror/mode/verilog/index.html | 4 +- .../plugins/codemirror/mode/verilog/test.js | 2 +- .../codemirror/mode/verilog/verilog.js | 428 ++++--- .../plugins/codemirror/mode/vhdl/index.html | 4 +- .../plugins/codemirror/mode/vhdl/vhdl.js | 2 +- .../plugins/codemirror/mode/vue/index.html | 4 +- .../vendor/plugins/codemirror/mode/vue/vue.js | 24 +- .../plugins/codemirror/mode/webidl/index.html | 4 +- .../plugins/codemirror/mode/webidl/webidl.js | 2 +- .../plugins/codemirror/mode/xml/index.html | 4 +- .../plugins/codemirror/mode/xml/test.js | 2 +- .../vendor/plugins/codemirror/mode/xml/xml.js | 23 +- .../plugins/codemirror/mode/xquery/index.html | 5 +- .../plugins/codemirror/mode/xquery/test.js | 12 +- .../plugins/codemirror/mode/xquery/xquery.js | 59 +- .../plugins/codemirror/mode/yacas/index.html | 2 +- .../plugins/codemirror/mode/yacas/yacas.js | 4 +- .../mode/yaml-frontmatter/index.html | 2 +- .../mode/yaml-frontmatter/yaml-frontmatter.js | 2 +- .../plugins/codemirror/mode/yaml/index.html | 2 +- .../plugins/codemirror/mode/yaml/yaml.js | 7 +- .../plugins/codemirror/mode/z80/index.html | 4 +- .../vendor/plugins/codemirror/mode/z80/z80.js | 2 +- 352 files changed, 14625 insertions(+), 2451 deletions(-) create mode 100644 public/vendor/plugins/codemirror/addon/comment/comment.js create mode 100644 public/vendor/plugins/codemirror/addon/comment/continuecomment.js create mode 100644 public/vendor/plugins/codemirror/addon/dialog/dialog.css create mode 100644 public/vendor/plugins/codemirror/addon/dialog/dialog.js create mode 100644 public/vendor/plugins/codemirror/addon/display/autorefresh.js create mode 100644 public/vendor/plugins/codemirror/addon/display/fullscreen.css create mode 100644 public/vendor/plugins/codemirror/addon/display/fullscreen.js create mode 100644 public/vendor/plugins/codemirror/addon/display/panel.js create mode 100644 public/vendor/plugins/codemirror/addon/display/placeholder.js create mode 100644 public/vendor/plugins/codemirror/addon/display/rulers.js create mode 100644 public/vendor/plugins/codemirror/addon/edit/closebrackets.js create mode 100644 public/vendor/plugins/codemirror/addon/edit/closetag.js create mode 100644 public/vendor/plugins/codemirror/addon/edit/continuelist.js create mode 100644 public/vendor/plugins/codemirror/addon/edit/matchbrackets.js create mode 100644 public/vendor/plugins/codemirror/addon/edit/matchtags.js create mode 100644 public/vendor/plugins/codemirror/addon/edit/trailingspace.js create mode 100644 public/vendor/plugins/codemirror/addon/fold/brace-fold.js create mode 100644 public/vendor/plugins/codemirror/addon/fold/comment-fold.js create mode 100644 public/vendor/plugins/codemirror/addon/fold/foldcode.js create mode 100644 public/vendor/plugins/codemirror/addon/fold/foldgutter.css create mode 100644 public/vendor/plugins/codemirror/addon/fold/foldgutter.js create mode 100644 public/vendor/plugins/codemirror/addon/fold/indent-fold.js create mode 100644 public/vendor/plugins/codemirror/addon/fold/markdown-fold.js create mode 100644 public/vendor/plugins/codemirror/addon/fold/xml-fold.js create mode 100644 public/vendor/plugins/codemirror/addon/hint/anyword-hint.js create mode 100644 public/vendor/plugins/codemirror/addon/hint/css-hint.js create mode 100644 public/vendor/plugins/codemirror/addon/hint/html-hint.js create mode 100644 public/vendor/plugins/codemirror/addon/hint/javascript-hint.js create mode 100644 public/vendor/plugins/codemirror/addon/hint/show-hint.css create mode 100644 public/vendor/plugins/codemirror/addon/hint/show-hint.js create mode 100644 public/vendor/plugins/codemirror/addon/hint/sql-hint.js create mode 100644 public/vendor/plugins/codemirror/addon/hint/xml-hint.js create mode 100644 public/vendor/plugins/codemirror/addon/lint/coffeescript-lint.js create mode 100644 public/vendor/plugins/codemirror/addon/lint/css-lint.js create mode 100644 public/vendor/plugins/codemirror/addon/lint/html-lint.js create mode 100644 public/vendor/plugins/codemirror/addon/lint/javascript-lint.js create mode 100644 public/vendor/plugins/codemirror/addon/lint/json-lint.js create mode 100644 public/vendor/plugins/codemirror/addon/lint/lint.css create mode 100644 public/vendor/plugins/codemirror/addon/lint/lint.js create mode 100644 public/vendor/plugins/codemirror/addon/lint/yaml-lint.js create mode 100644 public/vendor/plugins/codemirror/addon/merge/merge.css create mode 100644 public/vendor/plugins/codemirror/addon/merge/merge.js create mode 100644 public/vendor/plugins/codemirror/addon/runmode/colorize.js create mode 100644 public/vendor/plugins/codemirror/addon/runmode/runmode-standalone.js create mode 100644 public/vendor/plugins/codemirror/addon/runmode/runmode.js create mode 100644 public/vendor/plugins/codemirror/addon/runmode/runmode.node.js create mode 100644 public/vendor/plugins/codemirror/addon/scroll/annotatescrollbar.js create mode 100644 public/vendor/plugins/codemirror/addon/scroll/scrollpastend.js create mode 100644 public/vendor/plugins/codemirror/addon/scroll/simplescrollbars.css create mode 100644 public/vendor/plugins/codemirror/addon/scroll/simplescrollbars.js create mode 100644 public/vendor/plugins/codemirror/addon/search/jump-to-line.js create mode 100644 public/vendor/plugins/codemirror/addon/search/match-highlighter.js create mode 100644 public/vendor/plugins/codemirror/addon/search/matchesonscrollbar.css create mode 100644 public/vendor/plugins/codemirror/addon/search/matchesonscrollbar.js create mode 100644 public/vendor/plugins/codemirror/addon/search/search.js create mode 100644 public/vendor/plugins/codemirror/addon/search/searchcursor.js create mode 100644 public/vendor/plugins/codemirror/addon/selection/active-line.js create mode 100644 public/vendor/plugins/codemirror/addon/selection/mark-selection.js create mode 100644 public/vendor/plugins/codemirror/addon/selection/selection-pointer.js create mode 100644 public/vendor/plugins/codemirror/addon/tern/tern.css create mode 100644 public/vendor/plugins/codemirror/addon/tern/tern.js create mode 100644 public/vendor/plugins/codemirror/addon/tern/worker.js create mode 100644 public/vendor/plugins/codemirror/addon/wrap/hardwrap.js create mode 100644 public/vendor/plugins/codemirror/mode/clojure/test.js create mode 100644 public/vendor/plugins/codemirror/mode/cypher/test.js create mode 100644 public/vendor/plugins/codemirror/mode/d/test.js create mode 100644 public/vendor/plugins/codemirror/mode/dockerfile/test.js rename public/vendor/plugins/codemirror/mode/{jade => pug}/index.html (76%) rename public/vendor/plugins/codemirror/mode/{jade/jade.js => pug/pug.js} (98%) create mode 100644 public/vendor/plugins/codemirror/mode/sass/test.js create mode 100644 public/vendor/plugins/codemirror/mode/soy/test.js create mode 100644 public/vendor/plugins/codemirror/mode/swift/test.js 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; }