Compare commits

..

42 Commits

Author SHA1 Message Date
zeripath
07688231c2 Changelog for v1.11.2 (#10627)
* Changelog for v1.11.2

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* Update CHANGELOG.md

Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com>

* as per @jolheiser

* as per @jolheiser x2

* Update CHANGELOG.md

Co-Authored-By: John Olheiser <john.olheiser@gmail.com>

* Update CHANGELOG.md

Co-Authored-By: John Olheiser <john.olheiser@gmail.com>

* another security pr

* another security pr

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2020-03-06 15:38:11 +08:00
John Olheiser
21eaeb8418 Issue writers perms can modify issues (#10623) (#10626)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-03-06 04:42:20 +00:00
Lauris BH
9a929ad17f Handle deleted base branch in PR (#10618) (#10619)
Signed-off-by: Andrew Thornton <art27@cantab.net>
2020-03-05 22:23:34 +02:00
zeripath
c19ac41b34 Delete dependencies when deleting a repository (#10608) (#10616)
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lauris BH <lauris@nix.lv>

Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: zeripath <art27@cantab.net>
2020-03-05 13:56:53 +00:00
Lunny Xiao
fd85d31cb4 Ensure executable bit is kept on the web editor (#10607) (#10614)
Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com>

Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com>
2020-03-05 16:55:21 +08:00
guillep2k
c9e4d7a564 Update mergebase in pr checker (#10586) (#10605) 2020-03-04 18:56:09 -03:00
techknowlogick
9990430e32 Build gitea1.11 with go1.13 (#10584) 2020-03-03 07:09:27 +00:00
John Olheiser
6f5656ab0e Logout POST action (#10582) (#10585)
* Change logout to POST

* Update for redirect

* Revert octicon to font

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-03-03 13:54:15 +08:00
guillep2k
e4a876cee1 Admin page for managing user e-mail activation (#10557) (#10579)
* Admin page for managing user e-mail activation (#10557)

* Implement mail activation admin panel

* Apply suggestions by @lunny

* Add UI for user activated emails

* Prevent admin from self-deactivate; add modal

Co-authored-by: zeripath <art27@cantab.net>

* Fix pagination options downgrade

Co-authored-by: zeripath <art27@cantab.net>
2020-03-02 17:09:37 -03:00
guillep2k
abb534ba7a Fix migration bug on v96.go (#10572) (#10573)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2020-03-02 10:32:20 -06:00
James Lakin
65dceb6a40 Fix redirection path if Slack webhook channel is invalid (#10566)
The path to the hook config is already defined by orgRepoCtx
2020-03-02 13:54:07 +02:00
6543
db26f0aca9 head.tmpl og:image picture location (#10531) (#10556)
* head.tmpl og:image picture location (#10531)

og:image picture location

* CI.restart()

* CI.restart()

Co-authored-by: FreeCipher <admin@freecipher.com>
2020-03-01 15:33:18 -05:00
Andreas Shimokawa
76878fd69b Fix 404 after activating secondary email (backport of #10547) (#10553) 2020-03-01 13:27:13 +01:00
zeripath
3444fa2dd7 Fix appearance of unsigned sha box in view_list (#10543) (#10544)
Unfortunately the fix in #10511 was slightly incorrect and placed the
detail box at one level too far out.

Signed-off-by: Andrew Thornton <art27@cantab.net>
2020-02-29 17:20:32 +02:00
zeripath
caa2aeaa52 Show Signer in commit lists and add basic trust (#10425) (#10524)
Backport #10425
Backport #10511

* Show Signer in commit lists and add basic trust (#10425)

Show the avatar of the signer in the commit list pages as we do not
enforce that the signer is an author or committer. This makes it
clearer who has signed the commit.

Also display commits signed by non-members differently from
members and in particular make it clear when a non-member signer
is different from the committer to help reduce the risk of
spoofing.

Signed-off-by: Andrew Thornton <art27@cantab.net>

Fix the signing icon in the  view_list.tmpl page (#10511)

Co-Authored-By: silverwind <me@silverwind.io>
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
2020-02-28 14:18:02 -03:00
6543
11300ee582 Fix potential bugs (#10513) (#10518)
* use e if it is an option
* potential nil so check err first
* check err first
* m == nil already checked
2020-02-28 00:12:23 -03:00
John Olheiser
c6b78c3d31 Org action fixes and form cleanup (#10512) (#10514)
* More org fixes

* Move form action query to inputs

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-27 19:38:21 -06:00
zeripath
4c40aa5be9 Use [:space:] instead of \s (#10508) (#10509)
Backport #10508
2020-02-27 19:12:10 +00:00
6543
50f2e90b76 [BugFix] Avoid mailing explicit unwatched (#10475) (#10500)
* [BugFix] Avoid mailing explicit unwatched (#10475)
2020-02-27 11:42:51 -06:00
zeripath
5d11ccc9e1 Handle push rejection message in Merge & Web Editor (#10373) (#10497)
Backport #10373

* Handle push rejection message in Merge

* Fix sanitize, adjust message handling

* Handle push-rejection in webeditor CRUD too

Co-authored-by: Lauris BH <lauris@nix.lv>

Co-authored-by: Lauris BH <lauris@nix.lv>
2020-02-27 15:37:11 +02:00
guillep2k
93860af542 Fix SQLite concurrency problems by using BEGIN IMMEDIATE (#10368) (#10493) 2020-02-26 23:30:57 -03:00
James Lakin
7bf5834f2c Show the username as a fallback on feeds if full name is blank (#10461)
Co-authored-by: Lauris BH <lauris@nix.lv>
2020-02-26 20:59:07 -05:00
John Olheiser
1fbdd9335f Fix double PR notification from API (#10482) (#10486)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-26 22:39:39 +02:00
John Olheiser
e9061a537c Fix admin notices (#10480) (#10483)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-26 11:14:37 -06:00
John Olheiser
ed664a9e1d Change admin dashboard to POST (#10465) (#10466)
* Change admin dashboard to POST (#10465)

* Add form and convert to POST

* Redirect for flash

* Convert octicons back to fa for 1.11

Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-25 17:42:43 -06:00
John Olheiser
4cb18601ff Change action GETs to POST (#10462) (#10464)
* Change action GETs to POST

* submite = submit + smite

* No more # href

* Fix test

* Match other tests

* Explicit csrf

Signed-off-by: jolheiser <john.olheiser@gmail.com>

Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
2020-02-25 15:08:21 -06:00
oscar.lofwenhamn
3abb25166c Update markbates/goth (backport) (#10445)
Update markbates/goth to v1.61.2
- Fixes a JWT decoding issue in the OpenID provider
- Updates the GitHub provider to use the authorization header for authentication
- Updates the Twitch provider for Twitch's v5 API changes
- Adds the email and is_private_email fields to the Apple provider's GetUser implementation
- Modifies gothic to export a non-collidable context key for setting the Provider in a context.Context
- Adds new scopes to the Spotify provider
- Adds the IDToken from OpenID providers on the user struct
- Make Apple provider's SecretParams public
- Adds support for sign in with Apple, and drops support for Go versions 1.7 and 1.8
- Fixes the Slack provider's FetchURL logic to use the appropriate scope for the info it needs
Signed-off-by: Oscar LÃfwenhamn <oscar.lofwenhamn@cgi.com>
2020-02-24 13:19:25 -05:00
6543
9e6ad64d48 Trigger webhooks on issue label-change via API too (#10421) (#10439)
* trigger webhooks with api too

* fix comment

* notify report old too

* CI restart

* restart CI again

* remove duplicated code
2020-02-24 14:30:59 +00:00
Lunny Xiao
b51d7c459e Fix webhook bug (#10427) (#10432)
Co-authored-by: techknowlogick <matti@mdranta.net>
2020-02-24 01:48:30 -03:00
zeripath
d3b6f001fe Various fixes in login sources (#10428) (#10429)
Co-authored-by: guillep2k <18600385+guillep2k@users.noreply.github.com>
2020-02-23 22:46:17 +02:00
silverwind
e938f1d945 Add frontend/backend make targets, fix source release (#10325) (#10414)
* Add frontend/backend make targets, fix source release

- Add 'make backend' and 'make frontend' make targets which are used to
  build go and js/css/svg files respectively.

- The 'backend' target can be invoked without requiring Node.js to be
  present on the system if pre-built frontend assets are present like
  in the release source tarballs.

- Fix source releases missing 'dist' folders inside 'node_modules' which
  were erronously excluded from tar.

- Store VERSION in file VERSION for the release tarballs and prefer that
  file over git-derived version.

* fix release task

* fix typo

* fix another typo

Fixes: https://github.com/go-gitea/gitea/issues/10253
2020-02-22 08:51:58 -03:00
guillep2k
7284327a00 Prevent panic on merge to PR (#10403) (#10408)
If you attempt to merge to a branch which on a PR there will be a nil pointer error in the pull request checker.

This panic is uncaught and will bring down the gitea server.

This PR adds protection to prevent this.

Co-authored-by: zeripath <art27@cantab.net>
2020-02-21 22:53:32 +00:00
guillep2k
919f3f11e2 update crypto vendors (#10398)
Co-authored-by: @techknowlogick
2020-02-21 11:27:18 -03:00
guillep2k
3cee15e6f9 Ensure only own addresses are updated (#10397) (#10399) 2020-02-21 10:35:17 -03:00
Lunny Xiao
34e3644ada Fix wrong num closed issues on repository when close issue via commit (#10364) (#10380)
Co-authored-by: Lauris BH <lauris@nix.lv>

Co-authored-by: Lauris BH <lauris@nix.lv>
2020-02-20 15:52:19 +01:00
guillep2k
14bd120cdc Reading pull attachments should depend on read UnitTypePullRequests (#10346) (#10354)
Co-authored-by: zeripath <art27@cantab.net>
2020-02-18 21:02:50 -06:00
6543
3e40f8bebc Set max-width on review-box comment box (#10348) (#10353)
Co-authored-by: zeripath <art27@cantab.net>
2020-02-18 18:59:14 -06:00
zeripath
df5f1d9dca Prevent nil pointer in GetPullRequestCommitStatusState (#10342) (#10344)
Backport #10344 

Ensure that pr.HeadRepo is loaded before using it in GetPullRequestCommitStatusState.

Fixes error on merging with successful commit merge statuses.
2020-02-18 20:04:10 +00:00
John Olheiser
457ee1ab5a Fix status check enable (#10341) (#10343)
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2020-02-18 19:34:50 +00:00
zeripath
4f64688902 Truncate long commit message header (#10301) (#10319)
* Truncate long commit message header

* Fix overflow in view commit table

* Use @media less

* Further improvements

* Fix the commit message on small screens

* adjust width of minimal table
2020-02-18 08:51:39 +01:00
zeripath
117dcf1c02 Set the initial commit status to Success otherwise it will always be Pending (#10317) (#10318)
Backport #10317

The commit status code has a bug whereby setting the initial status to Pending means you can never have the status of Success - it should be set to Success.
2020-02-17 20:50:59 +00:00
mrsdizzie
4529a262c0 Don't manually replace whitespace during render (#10291) (#10315)
* Don't manually replace whitespace during render

For historical reasons Gitea manually alters the urlPrefix and replaces
a whitespace with a +. This Works for URLs, but we're also passing
urlPrefix to git calls and adding the + is breaking the tree path.

Goldmark will automatically convert a white space to the proper %20, so
we should leave the string as is which lets us pass it to git unmodified
and then let Goldmark fix it.

Also fixed separate bug in URLJoin I noticed while testing where it will
silently discard sections of a path that have # in them (possibly
others). We should just escape it first.

Fixes 10156

* Escape elems as well

* Revert "Escape elems as well"

This reverts commit 8bf49596fe.

* restart ci

* remove changes to URLJoin

* restart ci

Co-authored-by: techknowlogick <matti@mdranta.net>

Co-authored-by: techknowlogick <matti@mdranta.net>
2020-02-17 19:46:28 +02:00
233 changed files with 5539 additions and 1750 deletions
+1
View File
@@ -69,6 +69,7 @@ coverage.all
/yarn.lock
/public/js
/public/css
/VERSION
# Snapcraft
snap/.snapcraft/
+45
View File
@@ -4,6 +4,51 @@ 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.11.2](https://github.com/go-gitea/gitea/releases/tag/v1.11.2) - 2020-03-06
* BREAKING
* Various fixes in login sources (#10428) (#10429)
* SECURITY
* Ensure only own addresses are updated (#10397) (#10399)
* Logout POST action (#10582) (#10585)
* Org action fixes and form cleanup (#10512) (#10514)
* Change action GETs to POST (#10462) (#10464)
* Fix admin notices (#10480) (#10483)
* Change admin dashboard to POST (#10465) (#10466)
* Update markbates/goth (#10444) (#10445)
* Update crypto vendors (#10385) (#10398)
* BUGFIXES
* Allow users with write permissions to modify issue descriptions and comments. (#10623) (#10626)
* Handle deleted base branch in PR (#10618) (#10619)
* Delete dependencies when deleting a repository (#10608) (#10616)
* Ensure executable bit is kept on the web editor (#10607) (#10614)
* Update mergebase in pr checker (#10586) (#10605)
* Fix release attachments being deleted while upgrading (#10572) (#10573)
* Fix redirection path if Slack webhook channel is invalid (#10566)
* Fix head.tmpl og:image picture location (#10531) (#10556)
* Fix 404 after activating secondary email (#10547) (#10553)
* Show Signer in commit lists and add basic trust (#10425 & #10511) (#10524)
* Fix potential bugs (#10513) (#10518)
* Use \[:space:\] instead of \\s (#10508) (#10509)
* Avoid mailing users that have explicitly unwatched an issue (#10475) (#10500)
* Handle push rejection message in Merge & Web Editor (#10373) (#10497)
* Fix SQLite concurrency problems by using BEGIN IMMEDIATE (#10368) (#10493)
* Fix double PR notification from API (#10482) (#10486)
* Show the username as a fallback on feeds if full name is blank (#10461)
* Trigger webhooks on issue label-change via API too (#10421) (#10439)
* Fix git reference type in webhooks (#10427) (#10432)
* Prevent panic on merge to PR (#10403) (#10408)
* Fix wrong num closed issues on repository when close issue via commit… (#10364) (#10380)
* Reading pull attachments should depend on read UnitTypePullRequests (#10346) (#10354)
* Set max-width on review-box comment box (#10348) (#10353)
* Prevent nil pointer in GetPullRequestCommitStatusState (#10342) (#10344)
* Fix protected branch status check settings (#10341) (#10343)
* Truncate long commit message header (#10301) (#10319)
* Set the initial commit status to Success otherwise it will always be Pending (#10317) (#10318)
* Don't manually replace whitespace during render (#10291) (#10315)
* ENHANCEMENT
* Admin page for managing user e-mail activation (#10557) (#10579)
## [1.11.1](https://github.com/go-gitea/gitea/releases/tag/v1.11.1) - 2020-02-15
* BUGFIXES
+36 -18
View File
@@ -29,6 +29,8 @@ EXTRA_GOFLAGS ?=
MAKE_VERSION := $(shell $(MAKE) -v | head -n 1)
STORED_VERSION_FILE := VERSION
ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
GITEA_VERSION ?= $(VERSION)
@@ -38,7 +40,13 @@ else
else
VERSION ?= master
endif
GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
ifneq ($(STORED_VERSION),)
GITEA_VERSION ?= $(STORED_VERSION)
else
GITEA_VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
endif
endif
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
@@ -96,13 +104,15 @@ include docker/Makefile
help:
@echo "Make Routines:"
@echo " - \"\" equivalent to \"build\""
@echo " - build creates the entire project"
@echo " - clean delete integration files and build files but not css and js files"
@echo " - clean-all delete all generated files (integration test, build, css and js files)"
@echo " - build build everything"
@echo " - frontend build frontend files"
@echo " - backend build backend files"
@echo " - clean delete backend and integration files"
@echo " - clean-all delete backend, frontend and integration files"
@echo " - css rebuild only css files"
@echo " - js rebuild only js files"
@echo " - generate run \"make css js\" and \"go generate\""
@echo " - fmt format the code"
@echo " - generate run \"go generate\""
@echo " - fmt format the Go code"
@echo " - generate-swagger generate the swagger spec from code comments"
@echo " - swagger-validate check if the swagger spec is valide"
@echo " - revive run code linter revive"
@@ -113,7 +123,7 @@ help:
.PHONY: go-check
go-check:
$(eval GO_VERSION := $(shell printf "%03d%03d%03d" $(shell go version | grep -Eo '[0-9]+\.?[0-9]+?\.?[0-9]?\s' | tr '.' ' ');))
$(eval GO_VERSION := $(shell printf "%03d%03d%03d" $(shell go version | grep -Eo '[0-9]+\.?[0-9]+?\.?[0-9]?[[:space:]]' | tr '.' ' ');))
@if [ "$(GO_VERSION)" -lt "001011000" ]; then \
echo "Gitea requires Go 1.11.0 or greater to build. You can get it at https://golang.org/dl/"; \
exit 1; \
@@ -156,10 +166,6 @@ fmt:
vet:
$(GO) vet $(PACKAGES)
.PHONY: generate
generate: js css
GO111MODULE=on $(GO) generate -mod=vendor $(PACKAGES)
.PHONY: generate-swagger
generate-swagger:
$(SWAGGER) generate spec -o './$(SWAGGER_SPEC)'
@@ -414,13 +420,23 @@ install: $(wildcard *.go)
$(GO) install -v -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)'
.PHONY: build
build: go-check generate $(EXECUTABLE)
build: frontend backend
.PHONY: frontend
frontend: node-check js css
.PHONY: backend
backend: go-check generate $(EXECUTABLE)
.PHONY: generate
generate:
GO111MODULE=on $(GO) generate -mod=vendor $(PACKAGES)
$(EXECUTABLE): $(GO_SOURCES)
GO111MODULE=on $(GO) build -mod=vendor $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
.PHONY: release
release: generate release-dirs release-windows release-linux release-darwin release-copy release-compress release-sources release-check
release: frontend generate release-dirs release-windows release-linux release-darwin release-copy release-compress release-sources release-check
.PHONY: release-dirs
release-dirs:
@@ -431,7 +447,7 @@ release-windows:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u src.techknowlogick.com/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
xgo -go go-1.13 -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
@@ -441,7 +457,7 @@ release-linux:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u src.techknowlogick.com/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out gitea-$(VERSION) .
xgo -go go-1.13 -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
@@ -451,7 +467,7 @@ release-darwin:
@hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u src.techknowlogick.com/xgo; \
fi
xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out gitea-$(VERSION) .
xgo -go go-1.13 -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin/*' -out gitea-$(VERSION) .
ifeq ($(CI),drone)
cp /build/* $(DIST)/binaries
endif
@@ -472,8 +488,10 @@ release-compress:
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && gxz -k -9 $${file}; done;
.PHONY: release-sources
release-sources:
tar cvzf $(DIST)/release/gitea-src-$(VERSION).tar.gz --exclude $(DIST) --exclude .git .
release-sources: | node_modules
echo $(VERSION) > $(STORED_VERSION_FILE)
tar --exclude=./$(DIST) --exclude=./.git --exclude=./node_modules/.cache -czf $(DIST)/release/gitea-src-$(VERSION).tar.gz .
rm -f $(STORED_VERSION_FILE)
node_modules: package-lock.json
npm install --no-save
+9
View File
@@ -33,6 +33,15 @@ From the root of the source tree, run:
TAGS="bindata" make build
The `build` target is split into two sub-targets:
- `make backend` which requires [Go 1.11](https://golang.org/dl/) or greater.
- `make frontend` which requires [Node.js 10.0.0](https://nodejs.org/en/download/) or greater.
If pre-built frontend files are present it is possible to only build the backend:
TAGS="bindata" make backend
More info: https://docs.gitea.io/en-us/install-from-source/
## Using
+1 -1
View File
@@ -18,7 +18,7 @@ params:
description: Git with a cup of tea
author: The Gitea Authors
website: https://docs.gitea.io
version: 1.11.0
version: 1.11.2
outputs:
home:
@@ -60,7 +60,7 @@ _Symbols used in table:_
| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | | ✓ |
| Group Milestones | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
| Granular user roles (Code, Issues, Wiki etc) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
| Verified Committer | | ✘ | ? | ✓ | ✓ | ✓ | ✘ |
| Verified Committer | | ✘ | ? | ✓ | ✓ | ✓ | ✘ |
| GPG Signed Commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Reject unsigned commits | [](https://github.com/go-gitea/gitea/issues/2770) | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ |
| Repository Activity page | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
@@ -114,6 +114,17 @@ recommended way to build from source is therefore:
TAGS="bindata sqlite sqlite_unlock_notify" make build
```
The `build` target is split into two sub-targets:
- `make backend` which requires [Go 1.11](https://golang.org/dl/) or greater.
- `make frontend` which requires [Node.js 10.0.0](https://nodejs.org/en/download/) or greater.
If pre-built frontend files are present it is possible to only build the backend:
```bash
TAGS="bindata" make backend
``
## Test
After following the steps above, a `gitea` binary will be available in the working directory.
+3 -3
View File
@@ -62,7 +62,7 @@ require (
github.com/lib/pq v1.2.0
github.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
github.com/mailru/easyjson v0.7.0 // indirect
github.com/markbates/goth v1.56.0
github.com/markbates/goth v1.61.2
github.com/mattn/go-isatty v0.0.7
github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d // indirect
github.com/mattn/go-sqlite3 v1.11.0
@@ -95,10 +95,10 @@ require (
github.com/yohcop/openid-go v0.0.0-20160914080427-2c050d2dae53
github.com/yuin/goldmark v1.1.19
go.etcd.io/bbolt v1.3.3 // indirect
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876
golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4
golang.org/x/net v0.0.0-20191101175033-0deb6923b6d9
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20191213221258-04c2e8eff935 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+7 -6
View File
@@ -351,6 +351,7 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lafriks/xormstore v1.3.2 h1:hqi3F8s/B4rz8GuEZZDuHuOxRjeuOpEI/cC7vcnWwH4=
github.com/lafriks/xormstore v1.3.2/go.mod h1:mVNIwIa25QIr8rfR7YlVjrqN/apswHkVdtLCyVYBzXw=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -370,8 +371,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.56.0 h1:XEYedCgMNz5pi3ojXI8z2XUmXtBnMeuKUpx4Z6HlNj8=
github.com/markbates/goth v1.56.0/go.mod h1:zZmAw0Es0Dpm7TT/4AdN14QrkiWLMrrU9Xei1o+/mdA=
github.com/markbates/goth v1.61.2 h1:jDowrUH5qw8KGuQdKwFhLzkXkTYCIPfz3LHADJsiPIs=
github.com/markbates/goth v1.61.2/go.mod h1:qh2QfwZoWRucQ+DR5KVKC6dUGkNCToWh4vS45GIzFsY=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-oci8 v0.0.0-20190320171441-14ba190cf52d h1:m+dSK37rFf2fqppZhg15yI2IwC9BtucBiRwSDm9VL8g=
@@ -581,8 +582,8 @@ golang.org/x/crypto v0.0.0-20190907121410-71b5226ff739/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad h1:5E5raQxcv+6CZ11RrBYQe5WRbUIWpScjh0kvHZkZIrQ=
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4 h1:4icQlpeqbz3WxfgP6Eq3szTj95KTrlH/CwzBzoxuFd0=
golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@@ -652,8 +653,8 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2 h1:/J2nHFg1MTqaRLFO7M+J78ASNsJoz3r0cvHBPQ77fsE=
golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ=
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+15 -7
View File
@@ -39,12 +39,12 @@ func TestAPICreateIssue(t *testing.T) {
defer prepareTestEnv(t)()
const body, title = "apiTestBody", "apiTestTitle"
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
repoBefore := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repoBefore.OwnerID}).(*models.User)
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repo.Name, token)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repoBefore.Name, token)
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
Body: body,
Title: title,
@@ -57,19 +57,23 @@ func TestAPICreateIssue(t *testing.T) {
assert.Equal(t, apiIssue.Title, title)
models.AssertExistsAndLoadBean(t, &models.Issue{
RepoID: repo.ID,
RepoID: repoBefore.ID,
AssigneeID: owner.ID,
Content: body,
Title: title,
})
repoAfter := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
assert.Equal(t, repoBefore.NumIssues+1, repoAfter.NumIssues)
assert.Equal(t, repoBefore.NumClosedIssues, repoAfter.NumClosedIssues)
}
func TestAPIEditIssue(t *testing.T) {
defer prepareTestEnv(t)()
issueBefore := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issueBefore.RepoID}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
repoBefore := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issueBefore.RepoID}).(*models.Repository)
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repoBefore.OwnerID}).(*models.User)
assert.NoError(t, issueBefore.LoadAttributes())
assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
assert.Equal(t, api.StateOpen, issueBefore.State())
@@ -84,7 +88,7 @@ func TestAPIEditIssue(t *testing.T) {
body := "new content!"
title := "new title from api set"
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repo.Name, issueBefore.Index, token)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
State: &issueState,
RemoveDeadline: &removeDeadline,
@@ -99,6 +103,7 @@ func TestAPIEditIssue(t *testing.T) {
DecodeJSON(t, resp, &apiIssue)
issueAfter := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
repoAfter := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issueBefore.RepoID}).(*models.Repository)
// check deleted user
assert.Equal(t, int64(500), issueAfter.PosterID)
@@ -107,6 +112,9 @@ func TestAPIEditIssue(t *testing.T) {
assert.Equal(t, int64(-1), issueBefore.PosterID)
assert.Equal(t, int64(-1), apiIssue.Poster.ID)
// check repo change
assert.Equal(t, repoBefore.NumClosedIssues+1, repoAfter.NumClosedIssues)
// API response
assert.Equal(t, api.StateClosed, apiIssue.State)
assert.Equal(t, milestone, apiIssue.Milestone.ID)
+11
View File
@@ -351,6 +351,17 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t)
assert.NoError(t, err)
})
t.Run("GenerateCommit", func(t *testing.T) {
_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
assert.NoError(t, err)
})
t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2"))
var pr2 api.PullRequest
t.Run("CreatePullRequest", func(t *testing.T) {
pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t)
assert.NoError(t, err)
})
t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index))
t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username))
@@ -126,7 +126,7 @@ func restoreOldDB(t *testing.T, version string) bool {
err := os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm)
assert.NoError(t, err)
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d", setting.Database.Path, setting.Database.Timeout))
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate", setting.Database.Path, setting.Database.Timeout))
assert.NoError(t, err)
defer db.Close()
+1 -1
View File
@@ -20,7 +20,7 @@ func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title st
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find("form").Attr("action")
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
assert.True(t, exists, "The template has changed")
postData := map[string]string{
+1 -1
View File
@@ -14,7 +14,7 @@ func TestSignOut(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user/logout")
req := NewRequest(t, "POST", "/user/logout")
session.MakeRequest(t, req, http.StatusFound)
// try to view a private repo, should fail
+6 -3
View File
@@ -122,10 +122,13 @@ func (a *Action) ShortActUserName() string {
return base.EllipsisString(a.GetActUserName(), 20)
}
// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME
// GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
func (a *Action) GetDisplayName() string {
if setting.UI.DefaultShowFullName {
return a.GetActFullName()
trimmedFullName := strings.TrimSpace(a.GetActFullName())
if len(trimmedFullName) > 0 {
return trimmedFullName
}
}
return a.ShortActUserName()
}
@@ -212,7 +215,7 @@ func (a *Action) getCommentLink(e Engine) string {
return "#"
}
if a.Comment == nil && a.CommentID != 0 {
a.Comment, _ = GetCommentByID(a.CommentID)
a.Comment, _ = getCommentByID(e, a.CommentID)
}
if a.Comment != nil {
return a.Comment.HTMLURL()
+6 -2
View File
@@ -79,7 +79,11 @@ func (a *Attachment) LinkedRepository() (*Repository, UnitType, error) {
return nil, UnitTypeIssues, err
}
repo, err := GetRepositoryByID(iss.RepoID)
return repo, UnitTypeIssues, err
unitType := UnitTypeIssues
if iss.IsPull {
unitType = UnitTypePullRequests
}
return repo, unitType, err
} else if a.ReleaseID != 0 {
rel, err := GetReleaseByID(a.ReleaseID)
if err != nil {
@@ -195,7 +199,7 @@ func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) {
func getAttachmentsByCommentID(e Engine, commentID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
return attachments, x.Where("comment_id=?", commentID).Find(&attachments)
return attachments, e.Where("comment_id=?", commentID).Find(&attachments)
}
// getAttachmentByReleaseIDFileName return a file based on the the following infos:
+1 -1
View File
@@ -138,7 +138,7 @@ func TestLinkedRepository(t *testing.T) {
expectedUnitType UnitType
}{
{"LinkedIssue", 1, &Repository{ID: 1}, UnitTypeIssues},
{"LinkedComment", 3, &Repository{ID: 1}, UnitTypeIssues},
{"LinkedComment", 3, &Repository{ID: 1}, UnitTypePullRequests},
{"LinkedRelease", 9, &Repository{ID: 1}, UnitTypeReleases},
{"Notlinked", 10, nil, -1},
}
+63
View File
@@ -7,6 +7,7 @@ package models
import (
"fmt"
"strings"
"code.gitea.io/gitea/modules/git"
)
@@ -56,6 +57,21 @@ func (err ErrNamePatternNotAllowed) Error() string {
return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
}
// ErrNameCharsNotAllowed represents a "character not allowed in name" error.
type ErrNameCharsNotAllowed struct {
Name string
}
// IsErrNameCharsNotAllowed checks if an error is an ErrNameCharsNotAllowed.
func IsErrNameCharsNotAllowed(err error) bool {
_, ok := err.(ErrNameCharsNotAllowed)
return ok
}
func (err ErrNameCharsNotAllowed) Error() string {
return fmt.Sprintf("User name is invalid [%s]: must be valid alpha or numeric or dash(-_) or dot characters", err.Name)
}
// ErrSSHDisabled represents an "SSH disabled" error.
type ErrSSHDisabled struct {
}
@@ -1355,6 +1371,53 @@ func (err ErrMergePushOutOfDate) Error() string {
return fmt.Sprintf("Merge PushOutOfDate Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// ErrPushRejected represents an error if merging fails due to rejection from a hook
type ErrPushRejected struct {
Style MergeStyle
Message string
StdOut string
StdErr string
Err error
}
// IsErrPushRejected checks if an error is a ErrPushRejected.
func IsErrPushRejected(err error) bool {
_, ok := err.(ErrPushRejected)
return ok
}
func (err ErrPushRejected) Error() string {
return fmt.Sprintf("Merge PushRejected Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut)
}
// GenerateMessage generates the remote message from the stderr
func (err *ErrPushRejected) GenerateMessage() {
messageBuilder := &strings.Builder{}
i := strings.Index(err.StdErr, "remote: ")
if i < 0 {
err.Message = ""
return
}
for {
if len(err.StdErr) <= i+8 {
break
}
if err.StdErr[i:i+8] != "remote: " {
break
}
i += 8
nl := strings.IndexByte(err.StdErr[i:], '\n')
if nl > 0 {
messageBuilder.WriteString(err.StdErr[i : i+nl+1])
i = i + nl + 1
} else {
messageBuilder.WriteString(err.StdErr[i:])
i = len(err.StdErr)
}
}
err.Message = strings.TrimSpace(messageBuilder.String())
}
// ErrRebaseConflicts represents an error if rebase fails with a conflict
type ErrRebaseConflicts struct {
Style MergeStyle
+40 -3
View File
@@ -369,6 +369,7 @@ type CommitVerification struct {
CommittingUser *User
SigningEmail string
SigningKey *GPGKey
TrustStatus string
}
// SignCommit represents a commit with validation of signature.
@@ -754,18 +755,54 @@ func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature,
}
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
func ParseCommitsWithSignature(oldCommits *list.List) *list.List {
func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *list.List {
var (
newCommits = list.New()
e = oldCommits.Front()
)
memberMap := map[int64]bool{}
for e != nil {
c := e.Value.(UserCommit)
newCommits.PushBack(SignCommit{
signCommit := SignCommit{
UserCommit: &c,
Verification: ParseCommitWithSignature(c.Commit),
})
}
_ = CalculateTrustStatus(signCommit.Verification, repository, &memberMap)
newCommits.PushBack(signCommit)
e = e.Next()
}
return newCommits
}
// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, memberMap *map[int64]bool) (err error) {
if verification.Verified {
verification.TrustStatus = "trusted"
if verification.SigningUser.ID != 0 {
var isMember bool
if memberMap != nil {
var has bool
isMember, has = (*memberMap)[verification.SigningUser.ID]
if !has {
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
(*memberMap)[verification.SigningUser.ID] = isMember
}
} else {
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
}
if !isMember {
verification.TrustStatus = "untrusted"
if verification.CommittingUser.ID != verification.SigningUser.ID {
// The committing user and the signing user are not the same and are not the default key
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
verification.TrustStatus = "unmatched"
}
}
}
}
return
}
+4
View File
@@ -673,6 +673,10 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (*C
return nil, err
}
if err := issue.updateClosedNum(e); err != nil {
return nil, err
}
// New action comment
cmtType := CommentTypeClose
if !issue.IsClosed {
+5 -1
View File
@@ -749,8 +749,12 @@ func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commi
// GetCommentByID returns the comment by given ID.
func GetCommentByID(id int64) (*Comment, error) {
return getCommentByID(x, id)
}
func getCommentByID(e Engine, id int64) (*Comment, error) {
c := new(Comment)
has, err := x.ID(id).Get(c)
has, err := e.ID(id).Get(c)
if err != nil {
return nil, err
} else if !has {
+8 -4
View File
@@ -64,14 +64,18 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool
return
}
// GetIssueWatchersIDs returns IDs of subscribers to a given issue id
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetIssueWatchersIDs(issueID int64) ([]int64, error) {
func GetIssueWatchersIDs(issueID int64, watching bool) ([]int64, error) {
return getIssueWatchersIDs(x, issueID, watching)
}
func getIssueWatchersIDs(e Engine, issueID int64, watching bool) ([]int64, error) {
ids := make([]int64, 0, 64)
return ids, x.Table("issue_watch").
return ids, e.Table("issue_watch").
Where("issue_id=?", issueID).
And("is_watching = ?", true).
And("is_watching = ?", watching).
Select("user_id").
Find(&ids)
}
+13 -14
View File
@@ -12,7 +12,6 @@ import (
"fmt"
"net/smtp"
"net/textproto"
"regexp"
"strings"
"code.gitea.io/gitea/modules/auth/ldap"
@@ -455,10 +454,6 @@ func composeFullName(firstname, surname, username string) string {
}
}
var (
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
)
// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
// and create a local user if success when enabled.
func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*User, error) {
@@ -503,10 +498,6 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*Use
if len(sr.Username) == 0 {
sr.Username = login
}
// Validate username make sure it satisfies requirement.
if alphaDashDotPattern.MatchString(sr.Username) {
return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", sr.Username)
}
if len(sr.Mail) == 0 {
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
@@ -666,7 +657,8 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC
// LoginViaPAM queries if login/password is valid against the PAM,
// and create a local user if success when enabled.
func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig) (*User, error) {
if err := pam.Auth(cfg.ServiceName, login, password); err != nil {
pamLogin, err := pam.Auth(cfg.ServiceName, login, password)
if err != nil {
if strings.Contains(err.Error(), "Authentication failure") {
return nil, ErrUserNotExist{0, login, 0}
}
@@ -677,14 +669,21 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
return user, nil
}
// Allow PAM sources with `@` in their name, like from Active Directory
username := pamLogin
idx := strings.Index(pamLogin, "@")
if idx > -1 {
username = pamLogin[:idx]
}
user = &User{
LowerName: strings.ToLower(login),
Name: login,
Email: login,
LowerName: strings.ToLower(username),
Name: username,
Email: pamLogin,
Passwd: password,
LoginType: LoginPAM,
LoginSource: sourceID,
LoginName: login,
LoginName: login, // This is what the user typed in
IsActive: true,
}
return user, CreateUser(user)
+32 -17
View File
@@ -26,23 +26,38 @@ func deleteOrphanedAttachments(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
err := sess.BufferSize(setting.Database.IterateBufferSize).
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)
if err := os.RemoveAll(models.AttachmentLocalPath(attachment.UUID)); err != nil {
return err
}
_, err := sess.ID(attachment.ID).NoAutoCondition().Delete(attachment)
return err
})
if err != nil {
return err
var limit = setting.Database.IterateBufferSize
if limit <= 0 {
limit = 50
}
return sess.Commit()
for {
attachements := make([]Attachment, 0, limit)
if err := sess.Where("`issue_id` = 0 and (`release_id` = 0 or `release_id` not in (select `id` from `release`))").
Cols("id, uuid").Limit(limit).
Asc("id").
Find(&attachements); err != nil {
return err
}
if len(attachements) == 0 {
return nil
}
var ids = make([]int64, 0, limit)
for _, attachment := range attachements {
ids = append(ids, attachment.ID)
}
if _, err := sess.In("id", ids).Delete(new(Attachment)); err != nil {
return err
}
for _, attachment := range attachements {
if err := os.RemoveAll(models.AttachmentLocalPath(attachment.UUID)); err != nil {
return err
}
}
if len(attachements) < limit {
return nil
}
}
}
+1 -1
View File
@@ -399,7 +399,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
// Create org.
org := &User{
Name: "All repo",
Name: "All_repo",
IsActive: true,
Type: UserTypeOrganization,
Visibility: structs.VisibleTypePublic,
+12
View File
@@ -1856,6 +1856,18 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
return err
}
// Dependencies for issues in this repository
if _, err = sess.In("issue_id", deleteCond).
Delete(&IssueDependency{}); err != nil {
return err
}
// Delete dependencies for issues in other repositories
if _, err = sess.In("dependency_id", deleteCond).
Delete(&IssueDependency{}); err != nil {
return err
}
if _, err = sess.In("issue_id", deleteCond).
Delete(&IssueUser{}); err != nil {
return err
+20
View File
@@ -202,3 +202,23 @@ func (repo *Repository) getRepoTeams(e Engine) (teams []*Team, err error) {
func (repo *Repository) GetRepoTeams() ([]*Team, error) {
return repo.getRepoTeams(x)
}
// IsOwnerMemberCollaborator checks if a provided user is the owner, a collaborator or a member of a team in a repository
func (repo *Repository) IsOwnerMemberCollaborator(userID int64) (bool, error) {
if repo.OwnerID == userID {
return true, nil
}
teamMember, err := x.Join("INNER", "team_repo", "team_repo.team_id = team_user.team_id").
Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id").
Where("team_repo.repo_id = ?", repo.ID).
And("team_unit.`type` = ?", UnitTypeCode).
And("team_user.uid = ?", userID).Table("team_user").Exist(&TeamUser{})
if err != nil {
return false, err
}
if teamMember {
return true, nil
}
return x.Get(&Collaboration{RepoID: repo.ID, UserID: userID})
}
+1 -1
View File
@@ -84,7 +84,7 @@ func MainTest(m *testing.M, pathToGiteaRoot string) {
func createTestEngine(fixturesDir string) error {
var err error
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
x, err = xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate")
if err != nil {
return err
}
+15 -4
View File
@@ -18,6 +18,7 @@ import (
"image/png"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
@@ -87,6 +88,9 @@ var (
// ErrUnsupportedLoginType login source is unknown error
ErrUnsupportedLoginType = errors.New("Login source is unknown")
// Characters prohibited in a user name (anything except A-Za-z0-9_.-)
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
)
// User represents the object of individual and member of organization.
@@ -708,9 +712,11 @@ func (u *User) DisplayName() string {
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
// returns username otherwise.
func (u *User) GetDisplayName() string {
trimmed := strings.TrimSpace(u.FullName)
if len(trimmed) > 0 && setting.UI.DefaultShowFullName {
return trimmed
if setting.UI.DefaultShowFullName {
trimmed := strings.TrimSpace(u.FullName)
if len(trimmed) > 0 {
return trimmed
}
}
return u.Name
}
@@ -870,6 +876,11 @@ func isUsableName(names, patterns []string, name string) error {
// IsUsableUsername returns an error when a username is reserved
func IsUsableUsername(name string) error {
// Validate username make sure it satisfies requirement.
if alphaDashDotPattern.MatchString(name) {
// Note: usually this error is normally caught up earlier in the UI
return ErrNameCharsNotAllowed{Name: name}
}
return isUsableName(reservedUsernames, reservedUserPatterns, name)
}
@@ -989,7 +1000,7 @@ func VerifyActiveEmailCode(code, email string) *EmailAddress {
data := com.ToStr(user.ID) + email + user.LowerName + user.Passwd + user.Rands
if base.VerifyTimeLimitCode(data, minutes, prefix) {
emailAddress := &EmailAddress{Email: email}
emailAddress := &EmailAddress{UID: user.ID, Email: email}
if has, _ := x.Get(emailAddress); has {
return emailAddress
}
+274 -19
View File
@@ -1,4 +1,5 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 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.
@@ -8,6 +9,12 @@ import (
"errors"
"fmt"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
var (
@@ -54,13 +61,66 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
if !isPrimaryFound {
emails = append(emails, &EmailAddress{
Email: u.Email,
IsActivated: true,
IsActivated: u.IsActive,
IsPrimary: true,
})
}
return emails, nil
}
// GetEmailAddressByID gets a user's email address by ID
func GetEmailAddressByID(uid, id int64) (*EmailAddress, error) {
// User ID is required for security reasons
email := &EmailAddress{ID: id, UID: uid}
if has, err := x.Get(email); err != nil {
return nil, err
} else if !has {
return nil, nil
}
return email, nil
}
func isEmailActive(e Engine, email string, userID, emailID int64) (bool, error) {
if len(email) == 0 {
return true, nil
}
// Can't filter by boolean field unless it's explicit
cond := builder.NewCond()
cond = cond.And(builder.Eq{"email": email}, builder.Neq{"id": emailID})
if setting.Service.RegisterEmailConfirm {
// Inactive (unvalidated) addresses don't count as active if email validation is required
cond = cond.And(builder.Eq{"is_activated": true})
}
em := EmailAddress{}
if has, err := e.Where(cond).Get(&em); has || err != nil {
if has {
log.Info("isEmailActive('%s',%d,%d) found duplicate in email ID %d", email, userID, emailID, em.ID)
}
return has, err
}
// Can't filter by boolean field unless it's explicit
cond = builder.NewCond()
cond = cond.And(builder.Eq{"email": email}, builder.Neq{"id": userID})
if setting.Service.RegisterEmailConfirm {
cond = cond.And(builder.Eq{"is_active": true})
}
us := User{}
if has, err := e.Where(cond).Get(&us); has || err != nil {
if has {
log.Info("isEmailActive('%s',%d,%d) found duplicate in user ID %d", email, userID, emailID, us.ID)
}
return has, err
}
return false, nil
}
func isEmailUsed(e Engine, email string) (bool, error) {
if len(email) == 0 {
return true, nil
@@ -118,31 +178,30 @@ func AddEmailAddresses(emails []*EmailAddress) error {
// Activate activates the email address to given user.
func (email *EmailAddress) Activate() error {
user, err := GetUserByID(email.UID)
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := email.updateActivation(sess, true); err != nil {
return err
}
return sess.Commit()
}
func (email *EmailAddress) updateActivation(e Engine, activate bool) error {
user, err := getUserByID(e, email.UID)
if err != nil {
return err
}
if user.Rands, err = GetUserSalt(); err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
email.IsActivated = activate
if _, err := e.ID(email.ID).Cols("is_activated").Update(email); err != nil {
return err
}
email.IsActivated = true
if _, err := sess.
ID(email.ID).
Cols("is_activated").
Update(email); err != nil {
return err
} else if err = updateUserCols(sess, user, "rands"); err != nil {
return err
}
return sess.Commit()
return updateUserCols(e, user, "rands")
}
// DeleteEmailAddress deletes an email address of given user.
@@ -201,7 +260,7 @@ func MakeEmailPrimary(email *EmailAddress) error {
}
// Make sure the former primary email doesn't disappear.
formerPrimaryEmail := &EmailAddress{Email: user.Email}
formerPrimaryEmail := &EmailAddress{UID: user.ID, Email: user.Email}
has, err = x.Get(formerPrimaryEmail)
if err != nil {
return err
@@ -228,3 +287,199 @@ func MakeEmailPrimary(email *EmailAddress) error {
return sess.Commit()
}
// SearchEmailOrderBy is used to sort the results from SearchEmails()
type SearchEmailOrderBy string
func (s SearchEmailOrderBy) String() string {
return string(s)
}
// Strings for sorting result
const (
SearchEmailOrderByEmail SearchEmailOrderBy = "emails.email ASC, is_primary DESC, sortid ASC"
SearchEmailOrderByEmailReverse SearchEmailOrderBy = "emails.email DESC, is_primary ASC, sortid DESC"
SearchEmailOrderByName SearchEmailOrderBy = "`user`.lower_name ASC, is_primary DESC, sortid ASC"
SearchEmailOrderByNameReverse SearchEmailOrderBy = "`user`.lower_name DESC, is_primary ASC, sortid DESC"
)
// SearchEmailOptions are options to search e-mail addresses for the admin panel
type SearchEmailOptions struct {
Page int
PageSize int // Can be smaller than or equal to setting.UI.ExplorePagingNum
Keyword string
SortType SearchEmailOrderBy
IsPrimary util.OptionalBool
IsActivated util.OptionalBool
}
// SearchEmailResult is an e-mail address found in the user or email_address table
type SearchEmailResult struct {
UID int64
Email string
IsActivated bool
IsPrimary bool
// From User
Name string
FullName string
}
// SearchEmails takes options i.e. keyword and part of email name to search,
// it returns results in given range and number of total results.
func SearchEmails(opts *SearchEmailOptions) ([]*SearchEmailResult, int64, error) {
// Unfortunately, UNION support for SQLite in xorm is currently broken, so we must
// build the SQL ourselves.
where := make([]string, 0, 5)
args := make([]interface{}, 0, 5)
emailsSQL := "(SELECT id as sortid, uid, email, is_activated, 0 as is_primary " +
"FROM email_address " +
"UNION ALL " +
"SELECT id as sortid, id AS uid, email, is_active AS is_activated, 1 as is_primary " +
"FROM `user` " +
"WHERE type = ?) AS emails"
args = append(args, UserTypeIndividual)
if len(opts.Keyword) > 0 {
// Note: % can be injected in the Keyword parameter, but it won't do any harm.
where = append(where, "(lower(`user`.full_name) LIKE ? OR `user`.lower_name LIKE ? OR emails.email LIKE ?)")
likeStr := "%" + strings.ToLower(opts.Keyword) + "%"
args = append(args, likeStr)
args = append(args, likeStr)
args = append(args, likeStr)
}
switch {
case opts.IsPrimary.IsTrue():
where = append(where, "emails.is_primary = ?")
args = append(args, true)
case opts.IsPrimary.IsFalse():
where = append(where, "emails.is_primary = ?")
args = append(args, false)
}
switch {
case opts.IsActivated.IsTrue():
where = append(where, "emails.is_activated = ?")
args = append(args, true)
case opts.IsActivated.IsFalse():
where = append(where, "emails.is_activated = ?")
args = append(args, false)
}
var whereStr string
if len(where) > 0 {
whereStr = "WHERE " + strings.Join(where, " AND ")
}
joinSQL := "FROM " + emailsSQL + " INNER JOIN `user` ON `user`.id = emails.uid " + whereStr
count, err := x.SQL("SELECT count(*) "+joinSQL, args...).Count()
if err != nil {
return nil, 0, fmt.Errorf("Count: %v", err)
}
orderby := opts.SortType.String()
if orderby == "" {
orderby = SearchEmailOrderByEmail.String()
}
querySQL := "SELECT emails.uid, emails.email, emails.is_activated, emails.is_primary, " +
"`user`.name, `user`.full_name " + joinSQL + " ORDER BY " + orderby
if opts.PageSize == 0 || opts.PageSize > setting.UI.ExplorePagingNum {
opts.PageSize = setting.UI.ExplorePagingNum
}
if opts.Page <= 0 {
opts.Page = 1
}
rows, err := x.SQL(querySQL, args...).Rows(new(SearchEmailResult))
if err != nil {
return nil, 0, fmt.Errorf("Emails: %v", err)
}
// Page manually because xorm can't handle Limit() with raw SQL
defer rows.Close()
emails := make([]*SearchEmailResult, 0, opts.PageSize)
skip := (opts.Page - 1) * opts.PageSize
for rows.Next() {
var email SearchEmailResult
if err := rows.Scan(&email); err != nil {
return nil, 0, err
}
if skip > 0 {
skip--
continue
}
emails = append(emails, &email)
if len(emails) == opts.PageSize {
break
}
}
return emails, count, err
}
// ActivateUserEmail will change the activated state of an email address,
// either primary (in the user table) or secondary (in the email_address table)
func ActivateUserEmail(userID int64, email string, primary, activate bool) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if primary {
// Activate/deactivate a user's primary email address
user := User{ID: userID, Email: email}
if has, err := sess.Get(&user); err != nil {
return err
} else if !has {
return fmt.Errorf("no such user: %d (%s)", userID, email)
}
if user.IsActive == activate {
// Already in the desired state; no action
return nil
}
if activate {
if used, err := isEmailActive(sess, email, userID, 0); err != nil {
return fmt.Errorf("isEmailActive(): %v", err)
} else if used {
return ErrEmailAlreadyUsed{Email: email}
}
}
user.IsActive = activate
if user.Rands, err = GetUserSalt(); err != nil {
return fmt.Errorf("generate salt: %v", err)
}
if err = updateUserCols(sess, &user, "is_active", "rands"); err != nil {
return fmt.Errorf("updateUserCols(): %v", err)
}
} else {
// Activate/deactivate a user's secondary email address
// First check if there's another user active with the same address
addr := EmailAddress{UID: userID, Email: email}
if has, err := sess.Get(&addr); err != nil {
return err
} else if !has {
return fmt.Errorf("no such email: %d (%s)", userID, email)
}
if addr.IsActivated == activate {
// Already in the desired state; no action
return nil
}
if activate {
if used, err := isEmailActive(sess, email, 0, addr.ID); err != nil {
return fmt.Errorf("isEmailActive(): %v", err)
} else if used {
return ErrEmailAlreadyUsed{Email: email}
}
}
if err = addr.updateActivation(sess, activate); err != nil {
return fmt.Errorf("updateActivation(): %v", err)
}
}
return sess.Commit()
}
+64
View File
@@ -7,6 +7,8 @@ package models
import (
"testing"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
@@ -169,3 +171,65 @@ func TestActivate(t *testing.T) {
assert.True(t, emails[2].IsActivated)
assert.True(t, emails[2].IsPrimary)
}
func TestListEmails(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
// Must find all users and their emails
opts := &SearchEmailOptions{}
emails, count, err := SearchEmails(opts)
assert.NoError(t, err)
assert.NotEqual(t, int64(0), count)
assert.True(t, count > 5)
contains := func(match func(s *SearchEmailResult) bool) bool {
for _, v := range emails {
if match(v) {
return true
}
}
return false
}
assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 18 }))
// 'user3' is an organization
assert.False(t, contains(func(s *SearchEmailResult) bool { return s.UID == 3 }))
// Must find no records
opts = &SearchEmailOptions{Keyword: "NOTFOUND"}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.Equal(t, int64(0), count)
// Must find users 'user2', 'user28', etc.
opts = &SearchEmailOptions{Keyword: "user2"}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.NotEqual(t, int64(0), count)
assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 2 }))
assert.True(t, contains(func(s *SearchEmailResult) bool { return s.UID == 27 }))
// Must find only primary addresses (i.e. from the `user` table)
opts = &SearchEmailOptions{IsPrimary: util.OptionalBoolTrue}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.True(t, contains(func(s *SearchEmailResult) bool { return s.IsPrimary }))
assert.False(t, contains(func(s *SearchEmailResult) bool { return !s.IsPrimary }))
// Must find only inactive addresses (i.e. not validated)
opts = &SearchEmailOptions{IsActivated: util.OptionalBoolFalse}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.True(t, contains(func(s *SearchEmailResult) bool { return !s.IsActivated }))
assert.False(t, contains(func(s *SearchEmailResult) bool { return s.IsActivated }))
// Must find more than one page, but retrieve only one
opts = &SearchEmailOptions{
PageSize: 5,
Page: 1,
}
emails, count, err = SearchEmails(opts)
assert.NoError(t, err)
assert.Equal(t, 5, len(emails))
assert.True(t, count > int64(len(emails)))
}
+10
View File
@@ -47,3 +47,13 @@ type AdminEditUserForm struct {
func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
// AdminDashboardForm form for admin dashboard operations
type AdminDashboardForm struct {
Op int `binding:"required"`
}
// Validate validates form fields
func (f *AdminDashboardForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
+6 -4
View File
@@ -13,7 +13,7 @@ import (
)
// Auth pam auth service
func Auth(serviceName, userName, passwd string) error {
func Auth(serviceName, userName, passwd string) (string, error) {
t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff:
@@ -25,12 +25,14 @@ func Auth(serviceName, userName, passwd string) error {
})
if err != nil {
return err
return "", err
}
if err = t.Authenticate(0); err != nil {
return err
return "", err
}
return nil
// PAM login names might suffer transformations in the PAM stack.
// We should take whatever the PAM stack returns for it.
return t.GetItem(pam.User)
}
+2 -2
View File
@@ -11,6 +11,6 @@ import (
)
// Auth not supported lack of pam tag
func Auth(serviceName, userName, passwd string) error {
return errors.New("PAM not supported")
func Auth(serviceName, userName, passwd string) (string, error) {
return "", errors.New("PAM not supported")
}
+1 -1
View File
@@ -108,7 +108,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
}
at := bytes.IndexByte(line, '@')
m = []int{0, stop, at, stop - 1}
if m == nil || bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
if bytes.IndexByte(line[m[2]:m[3]], '.') < 0 {
return nil
}
lastChar := line[m[1]-1]
-1
View File
@@ -52,7 +52,6 @@ func (g *GiteaASTTransformer) Transform(node *ast.Document, reader text.Reader,
lnk := string(link)
lnk = giteautil.URLJoin(prefix, lnk)
lnk = strings.Replace(lnk, " ", "+", -1)
link = []byte(lnk)
}
v.Destination = link
-1
View File
@@ -81,7 +81,6 @@ func RenderWiki(filename string, rawBytes []byte, urlPrefix string, metas map[st
}
func render(parser Parser, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
urlPrefix = strings.Replace(urlPrefix, " ", "+", -1)
result := parser.Render(rawBytes, urlPrefix, metas, isWiki)
// TODO: one day the error should be returned.
result, err := PostProcess(result, urlPrefix, metas, isWiki)
+2 -2
View File
@@ -689,12 +689,12 @@ func (m *webhookNotifier) NotifyDeleteRef(pusher *models.User, repo *models.Repo
if err := webhook_module.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{
Ref: refName,
RefType: "branch",
RefType: refType,
PusherType: api.PusherTypeUser,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
log.Error("PrepareWebhooks.(delete branch): %v", err)
log.Error("PrepareWebhooks.(delete %s): %v", refType, err)
}
}
+23 -3
View File
@@ -242,10 +242,30 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models
func (t *TemporaryUploadRepository) Push(doer *models.User, commitHash string, branch string) error {
// Because calls hooks we need to pass in the environment
env := models.PushingEnvironment(doer, t.repo)
stdout := &strings.Builder{}
stderr := &strings.Builder{}
if _, err := git.NewCommand("push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)).RunInDirWithEnv(t.basePath, env); err != nil {
log.Error("Unable to push back to repo from temporary repo: %s (%s) Error: %v",
t.repo.FullName(), t.basePath, err)
if err := git.NewCommand("push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)).RunInDirTimeoutEnvPipeline(env, -1, t.basePath, stdout, stderr); err != nil {
errString := stderr.String()
if strings.Contains(errString, "non-fast-forward") {
return models.ErrMergePushOutOfDate{
StdOut: stdout.String(),
StdErr: errString,
Err: err,
}
} else if strings.Contains(errString, "! [remote rejected]") {
log.Error("Unable to push back to repo from temporary repo due to rejection: %s (%s)\nStdout: %s\nStderr: %s\nError: %v",
t.repo.FullName(), t.basePath, stdout, errString, err)
err := models.ErrPushRejected{
StdOut: stdout.String(),
StdErr: errString,
Err: err,
}
err.GenerateMessage()
return err
}
log.Error("Unable to push back to repo from temporary repo: %s (%s)\nStdout: %s\nError: %v",
t.repo.FullName(), t.basePath, stdout, err)
return fmt.Errorf("Unable to push back to repo from temporary repo: %s (%s) Error: %v",
t.repo.FullName(), t.basePath, err)
}
+10 -2
View File
@@ -210,6 +210,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
encoding := "UTF-8"
bom := false
executable := false
if !opts.IsNewFile {
fromEntry, err := commit.GetTreeEntryByPath(fromTreePath)
@@ -245,6 +246,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
return nil, models.ErrSHAOrCommitIDNotProvided{}
}
encoding, bom = detectEncodingAndBOM(fromEntry, repo)
executable = fromEntry.IsExecutable()
}
// For the path where this file will be created/updated, we need to make
@@ -368,8 +370,14 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
}
// Add the object to the index
if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
return nil, err
if executable {
if err := t.AddObjectToIndex("100755", objectHash, treePath); err != nil {
return nil, err
}
} else {
if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
return nil, err
}
}
// Now write the tree
+1 -1
View File
@@ -124,7 +124,7 @@ func DBConnStr() (string, error) {
if err := os.MkdirAll(path.Dir(Database.Path), os.ModePerm); err != nil {
return "", fmt.Errorf("Failed to create directories: %v", err)
}
connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d", Database.Path, Database.Timeout)
connStr = fmt.Sprintf("file:%s?cache=shared&mode=rwc&_busy_timeout=%d&_txlock=immediate", Database.Path, Database.Timeout)
default:
return "", fmt.Errorf("Unknown database type: %s", Database.Type)
}
+26
View File
@@ -374,6 +374,7 @@ user_bio = Biography
form.name_reserved = The username '%s' is reserved.
form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username.
form.name_chars_not_allowed = User name '%s' contains invalid characters.
[settings]
profile = Profile
@@ -434,7 +435,11 @@ manage_openid = Manage OpenID Addresses
email_desc = Your primary email address will be used for notifications and other operations.
theme_desc = This will be your default theme across the site.
primary = Primary
activated = Activated
requires_activation = Requires activation
primary_email = Make Primary
activate_email = Send Activation
activations_pending = Activations Pending
delete_email = Remove
email_deletion = Remove Email Address
email_deletion_desc = The email address and related information will be removed from your account. Git commits by this email address will remain unchanged. Continue?
@@ -775,6 +780,8 @@ editor.commit_empty_file_header = Commit an empty file
editor.commit_empty_file_text = The file you're about commit is empty. Proceed?
editor.no_changes_to_show = There are no changes to show.
editor.fail_to_update_file = Failed to update/create file '%s' with error: %v
editor.push_rejected_no_message = The change was rejected by the server without a message. Please check githooks.
editor.push_rejected = The change was rejected by the server with the following message:<br>%s<br> Please check githooks.
editor.add_subdir = Add a directory…
editor.unable_to_upload_files = Failed to upload files to '%s' with error: %v
editor.upload_file_is_locked = File '%s' is locked by %s.
@@ -794,6 +801,8 @@ commits.date = Date
commits.older = Older
commits.newer = Newer
commits.signed_by = Signed by
commits.signed_by_untrusted_user = Signed by untrusted user
commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does not match committer
commits.gpg_key_id = GPG Key ID
ext_issues = Ext. Issues
@@ -1074,6 +1083,8 @@ pulls.merge_conflict = Merge Failed: There was a conflict whilst merging: %[1]s<
pulls.rebase_conflict = Merge Failed: There was a conflict whilst rebasing commit: %[1]s<br>%[2]s<br>%[3]s<br>Hint:Try a different strategy
pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy
pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again.
pulls.push_rejected = Merge Failed: The push was rejected with the following message:<br>%s<br>Review the githooks for this repository
pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.<br>Review the githooks for this repository
pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.`
pulls.status_checking = Some checks are pending
pulls.status_checks_success = All checks were successful
@@ -1681,6 +1692,7 @@ organizations = Organizations
repositories = Repositories
hooks = Default Webhooks
authentication = Authentication Sources
emails = User Emails
config = Configuration
notices = System Notices
monitor = Monitoring
@@ -1750,6 +1762,7 @@ dashboard.gc_times = GC Times
users.user_manage_panel = User Account Management
users.new_account = Create User Account
users.name = Username
users.full_name = Full Name
users.activated = Activated
users.admin = Admin
users.restricted = Restricted
@@ -1781,6 +1794,19 @@ users.still_own_repo = This user still owns one or more repositories. Delete or
users.still_has_org = This user is a member of an organization. Remove the user from any organizations first.
users.deletion_success = The user account has been deleted.
emails.email_manage_panel = User Email Management
emails.primary = Primary
emails.activated = Activated
emails.filter_sort.email = Email
emails.filter_sort.email_reverse = Email (reverse)
emails.filter_sort.name = User Name
emails.filter_sort.name_reverse = User Name (reverse)
emails.updated = Email updated
emails.not_updated = Failed to update the requested email address: %v
emails.duplicate_active = This email address is already active for a different user.
emails.change_email_header = Update Email Properties
emails.change_email_text = Are your sure you want to update this email address?
orgs.org_manage_panel = Organization Management
orgs.name = Name
orgs.teams = Teams
+19 -11
View File
@@ -16,6 +16,7 @@ import (
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/cron"
@@ -30,7 +31,6 @@ import (
"gitea.com/macaron/macaron"
"gitea.com/macaron/session"
"github.com/unknwon/com"
)
const (
@@ -144,14 +144,28 @@ func Dashboard(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["Stats"] = models.GetStatistic()
// FIXME: update periodically
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.HTML(200, tplDashboard)
}
// DashboardPost run an admin operation
func DashboardPost(ctx *context.Context, form auth.AdminDashboardForm) {
ctx.Data["Title"] = ctx.Tr("admin.dashboard")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminDashboard"] = true
ctx.Data["Stats"] = models.GetStatistic()
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
// Run operation.
op, _ := com.StrTo(ctx.Query("op")).Int()
if op > 0 {
if form.Op > 0 {
var err error
var success string
switch Operation(op) {
switch Operation(form.Op) {
case cleanInactivateUser:
success = ctx.Tr("admin.dashboard.delete_inactivate_accounts_success")
err = models.DeleteInactivateUsers()
@@ -189,15 +203,9 @@ func Dashboard(ctx *context.Context) {
} else {
ctx.Flash.Success(success)
}
ctx.Redirect(setting.AppSubURL + "/admin")
return
}
ctx.Data["Stats"] = models.GetStatistic()
// FIXME: update periodically
updateSystemStatus()
ctx.Data["SysStatus"] = sysStatus
ctx.HTML(200, tplDashboard)
ctx.Redirect(setting.AppSubURL + "/admin")
}
// SendTestMail send test mail to confirm mail service is OK
+155
View File
@@ -0,0 +1,155 @@
// Copyright 2020 The Gitea Authors.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package admin
import (
"bytes"
"net/url"
"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/unknwon/com"
)
const (
tplEmails base.TplName = "admin/emails/list"
)
// Emails show all emails
func Emails(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.emails")
ctx.Data["PageIsAdmin"] = true
ctx.Data["PageIsAdminEmails"] = true
opts := &models.SearchEmailOptions{
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.QueryInt("page"),
}
if opts.Page <= 1 {
opts.Page = 1
}
type ActiveEmail struct {
models.SearchEmailResult
CanChange bool
}
var (
baseEmails []*models.SearchEmailResult
emails []ActiveEmail
count int64
err error
orderBy models.SearchEmailOrderBy
)
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "email":
orderBy = models.SearchEmailOrderByEmail
case "reverseemail":
orderBy = models.SearchEmailOrderByEmailReverse
case "username":
orderBy = models.SearchEmailOrderByName
case "reverseusername":
orderBy = models.SearchEmailOrderByNameReverse
default:
ctx.Data["SortType"] = "email"
orderBy = models.SearchEmailOrderByEmail
}
opts.Keyword = ctx.QueryTrim("q")
opts.SortType = orderBy
if len(ctx.Query("is_activated")) != 0 {
opts.IsActivated = util.OptionalBoolOf(ctx.QueryBool("activated"))
}
if len(ctx.Query("is_primary")) != 0 {
opts.IsPrimary = util.OptionalBoolOf(ctx.QueryBool("primary"))
}
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
baseEmails, count, err = models.SearchEmails(opts)
if err != nil {
ctx.ServerError("SearchEmails", err)
return
}
emails = make([]ActiveEmail, len(baseEmails))
for i := range baseEmails {
emails[i].SearchEmailResult = *baseEmails[i]
// Don't let the admin deactivate its own primary email address
// We already know the user is admin
emails[i].CanChange = ctx.User.ID != emails[i].UID || !emails[i].IsPrimary
}
}
ctx.Data["Keyword"] = opts.Keyword
ctx.Data["Total"] = count
ctx.Data["Emails"] = emails
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager
ctx.HTML(200, tplEmails)
}
var (
nullByte = []byte{0x00}
)
func isKeywordValid(keyword string) bool {
return !bytes.Contains([]byte(keyword), nullByte)
}
// ActivateEmail serves a POST request for activating/deactivating a user's email
func ActivateEmail(ctx *context.Context) {
truefalse := map[string]bool{"1": true, "0": false}
uid := com.StrTo(ctx.Query("uid")).MustInt64()
email := ctx.Query("email")
primary, okp := truefalse[ctx.Query("primary")]
activate, oka := truefalse[ctx.Query("activate")]
if uid == 0 || len(email) == 0 || !okp || !oka {
ctx.Error(400)
return
}
log.Info("Changing activation for User ID: %d, email: %s, primary: %v to %v", uid, email, primary, activate)
if err := models.ActivateUserEmail(uid, email, primary, activate); err != nil {
log.Error("ActivateUserEmail(%v,%v,%v,%v): %v", uid, email, primary, activate, err)
if models.IsErrEmailAlreadyUsed(err) {
ctx.Flash.Error(ctx.Tr("admin.emails.duplicate_active"))
} else {
ctx.Flash.Error(ctx.Tr("admin.emails.not_updated", err))
}
} else {
log.Info("Activation for User ID: %d, email: %s, primary: %v changed to %v", uid, email, primary, activate)
ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
}
redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")
q := url.Values{}
if val := ctx.QueryTrim("q"); len(val) > 0 {
q.Set("q", val)
}
if val := ctx.QueryTrim("sort"); len(val) > 0 {
q.Set("sort", val)
}
if val := ctx.QueryTrim("is_primary"); len(val) > 0 {
q.Set("is_primary", val)
}
if val := ctx.QueryTrim("is_activated"); len(val) > 0 {
q.Set("is_activated", val)
}
redirect.RawQuery = q.Encode()
ctx.Redirect(redirect.String())
}
+3
View File
@@ -121,6 +121,9 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) {
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
case models.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
default:
ctx.ServerError("CreateUser", err)
}
+1
View File
@@ -66,6 +66,7 @@ func CreateOrg(ctx *context.APIContext, form api.CreateOrgOption) {
if err := models.CreateOrganization(org, u); err != nil {
if models.IsErrUserAlreadyExist(err) ||
models.IsErrNameReserved(err) ||
models.IsErrNameCharsNotAllowed(err) ||
models.IsErrNamePatternNotAllowed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
+1
View File
@@ -90,6 +90,7 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) {
if models.IsErrUserAlreadyExist(err) ||
models.IsErrEmailAlreadyUsed(err) ||
models.IsErrNameReserved(err) ||
models.IsErrNameCharsNotAllowed(err) ||
models.IsErrNamePatternNotAllowed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
+1
View File
@@ -112,6 +112,7 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) {
if err := models.CreateOrganization(org, ctx.User); err != nil {
if models.IsErrUserAlreadyExist(err) ||
models.IsErrNameReserved(err) ||
models.IsErrNameCharsNotAllowed(err) ||
models.IsErrNamePatternNotAllowed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
+29 -36
View File
@@ -102,24 +102,8 @@ func AddIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) {
// "403":
// "$ref": "#/responses/forbidden"
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
issue, labels, err := prepareForReplaceOrAdd(ctx, form)
if err != nil {
if models.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
return
}
@@ -204,7 +188,7 @@ func DeleteIssueLabel(ctx *context.APIContext) {
return
}
if err := models.DeleteIssueLabel(issue, label, ctx.User); err != nil {
if err := issue_service.RemoveLabel(issue, ctx.User, label); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteIssueLabel", err)
return
}
@@ -248,28 +232,12 @@ func ReplaceIssueLabels(ctx *context.APIContext, form api.IssueLabelsOption) {
// "403":
// "$ref": "#/responses/forbidden"
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
issue, labels, err := prepareForReplaceOrAdd(ctx, form)
if err != nil {
if models.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
labels, err := models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
return
}
if err := issue.ReplaceLabels(labels, ctx.User); err != nil {
if err := issue_service.ReplaceLabels(issue, ctx.User, labels); err != nil {
ctx.Error(http.StatusInternalServerError, "ReplaceLabels", err)
return
}
@@ -339,3 +307,28 @@ func ClearIssueLabels(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent)
}
func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (issue *models.Issue, labels []*models.Label, err error) {
issue, err = models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil {
if models.IsErrIssueNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
}
return
}
labels, err = models.GetLabelsInRepoByIDs(ctx.Repo.Repository.ID, form.Labels)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelsInRepoByIDs", err)
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
return
}
+8 -3
View File
@@ -15,7 +15,6 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/notification"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
issue_service "code.gitea.io/gitea/services/issue"
@@ -316,8 +315,6 @@ func CreatePullRequest(ctx *context.APIContext, form api.CreatePullRequestOption
return
}
notification.NotifyNewPullRequest(pr)
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
ctx.JSON(http.StatusCreated, pr.APIFormat())
}
@@ -648,6 +645,14 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) {
} else if models.IsErrMergePushOutOfDate(err) {
ctx.Error(http.StatusConflict, "Merge", "merge push out of date")
return
} else if models.IsErrPushRejected(err) {
errPushRej := err.(models.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.Error(http.StatusConflict, "Merge", "PushRejected without remote error message")
return
}
ctx.Error(http.StatusConflict, "Merge", "PushRejected with remote message: "+errPushRej.Message)
return
}
ctx.Error(http.StatusInternalServerError, "Merge", err)
return
+2
View File
@@ -511,6 +511,8 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteA
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit()))
case models.IsErrNameReserved(err):
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name))
case models.IsErrNameCharsNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' contains invalid characters.", err.(models.ErrNameCharsNotAllowed).Name))
case models.IsErrNamePatternNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern))
default:
+4 -4
View File
@@ -70,14 +70,14 @@ func UploadAttachment(ctx *context.Context) {
func DeleteAttachment(ctx *context.Context) {
file := ctx.Query("file")
attach, err := models.GetAttachmentByUUID(file)
if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
ctx.Error(403)
return
}
if err != nil {
ctx.Error(400, err.Error())
return
}
if !ctx.IsSigned || (ctx.User.ID != attach.UploaderID) {
ctx.Error(403)
return
}
err = models.DeleteAttachment(attach, true)
if err != nil {
ctx.Error(500, fmt.Sprintf("DeleteAttachment: %v", err))
+11 -5
View File
@@ -65,7 +65,7 @@ func Commits(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
@@ -134,7 +134,7 @@ func SearchCommits(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
@@ -180,7 +180,7 @@ func FileHistory(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
@@ -237,11 +237,11 @@ func Diff(ctx *context.Context) {
parents := make([]string, commit.ParentCount())
for i := 0; i < commit.ParentCount(); i++ {
sha, err := commit.ParentID(i)
parents[i] = sha.String()
if err != nil {
ctx.NotFound("repo.Diff", err)
return
}
parents[i] = sha.String()
}
ctx.Data["CommitID"] = commitID
@@ -262,12 +262,18 @@ func Diff(ctx *context.Context) {
setPathsCompareContext(ctx, parentCommit, commit, headTarget)
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit
ctx.Data["Verification"] = models.ParseCommitWithSignature(commit)
verification := models.ParseCommitWithSignature(commit)
ctx.Data["Verification"] = verification
ctx.Data["Author"] = models.ValidateCommitWithEmail(commit)
ctx.Data["Diff"] = diff
ctx.Data["Parents"] = parents
ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0
if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
note := &git.Note{}
err = git.GetNote(ctx.Repo.GitRepo, commitID, note)
if err == nil {
+1 -1
View File
@@ -316,7 +316,7 @@ func PrepareCompareDiff(
}
compareInfo.Commits = models.ValidateCommitsWithEmails(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits, headRepo)
compareInfo.Commits = models.ParseCommitsWithStatus(compareInfo.Commits, headRepo)
ctx.Data["Commits"] = compareInfo.Commits
ctx.Data["CommitCount"] = compareInfo.Commits.Len()
+18 -3
View File
@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/upload"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/utils"
)
const (
@@ -262,10 +263,17 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
} else {
ctx.Error(500, err.Error())
}
} else if models.IsErrCommitIDDoesNotMatch(err) {
} else if models.IsErrCommitIDDoesNotMatch(err) || models.IsErrMergePushOutOfDate(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
} else if models.IsErrPushRejected(err) {
errPushRej := err.(models.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
} else {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected", utils.SanitizeFlashErrorString(errPushRej.Message)), tplEditFile, &form)
}
} else {
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, err), tplEditFile, &form)
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, utils.SanitizeFlashErrorString(err.Error())), tplEditFile, &form)
}
}
@@ -426,8 +434,15 @@ func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
} else {
ctx.Error(500, err.Error())
}
} else if models.IsErrCommitIDDoesNotMatch(err) {
} else if models.IsErrCommitIDDoesNotMatch(err) || models.IsErrMergePushOutOfDate(err) {
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplDeleteFile, &form)
} else if models.IsErrPushRejected(err) {
errPushRej := err.(models.ErrPushRejected)
if len(errPushRej.Message) == 0 {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
} else {
ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected", utils.SanitizeFlashErrorString(errPushRej.Message)), tplDeleteFile, &form)
}
} else {
ctx.ServerError("DeleteRepoFile", err)
}
+27 -13
View File
@@ -10,7 +10,6 @@ import (
"container/list"
"crypto/subtle"
"fmt"
"html"
"net/http"
"path"
"strings"
@@ -25,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/repofiles"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/gitdiff"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
@@ -354,6 +354,16 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
return nil
}
defer baseGitRepo.Close()
if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["HeadTarget"] = pull.HeadBranch
ctx.Data["NumCommits"] = 0
ctx.Data["NumFiles"] = 0
return nil
}
var headBranchExist bool
var headBranchSha string
// HeadRepo may be missing
@@ -477,7 +487,7 @@ func ViewPullCommits(ctx *context.Context) {
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
commits = prInfo.Commits
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = commits.Len()
@@ -586,6 +596,8 @@ func ViewPullFiles(ctx *context.Context) {
return
}
getBranchData(ctx, issue)
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
ctx.Data["IsIssueWriter"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
ctx.HTML(200, tplPullFiles)
}
@@ -663,27 +675,18 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
}
if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil {
sanitize := func(x string) string {
runes := []rune(x)
if len(runes) > 512 {
x = "..." + string(runes[len(runes)-512:])
}
return strings.Replace(html.EscapeString(x), "\n", "<br>", -1)
}
if models.IsErrInvalidMergeStyle(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
} else if models.IsErrMergeConflicts(err) {
conflictError := err.(models.ErrMergeConflicts)
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", sanitize(conflictError.StdErr), sanitize(conflictError.StdOut)))
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", utils.SanitizeFlashErrorString(conflictError.StdErr), utils.SanitizeFlashErrorString(conflictError.StdOut)))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
} else if models.IsErrRebaseConflicts(err) {
conflictError := err.(models.ErrRebaseConflicts)
ctx.Flash.Error(ctx.Tr("repo.pulls.rebase_conflict", sanitize(conflictError.CommitSHA), sanitize(conflictError.StdErr), sanitize(conflictError.StdOut)))
ctx.Flash.Error(ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA), utils.SanitizeFlashErrorString(conflictError.StdErr), utils.SanitizeFlashErrorString(conflictError.StdOut)))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
} else if models.IsErrMergeUnrelatedHistories(err) {
@@ -696,6 +699,17 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) {
ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
} else if models.IsErrPushRejected(err) {
log.Debug("MergePushRejected error: %v", err)
pushrejErr := err.(models.ErrPushRejected)
message := pushrejErr.Message
if len(message) == 0 {
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
} else {
ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected", utils.SanitizeFlashErrorString(pushrejErr.Message)))
}
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
return
}
ctx.ServerError("Merge", err)
return
+8 -1
View File
@@ -181,7 +181,14 @@ func renderDirectory(ctx *context.Context, treeLink string) {
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
ctx.Data["LatestCommit"] = latestCommit
ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
verification := models.ParseCommitWithSignature(latestCommit)
if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)
statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository, ctx.Repo.Commit.ID.String(), 0)
+2 -2
View File
@@ -448,7 +448,7 @@ func SlackHooksNewPost(ctx *context.Context, form auth.NewSlackHookForm) {
if form.HasInvalidChannel() {
ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
ctx.Redirect(orCtx.Link + "/settings/hooks/slack/new")
ctx.Redirect(orCtx.Link + "/slack/new")
return
}
@@ -642,7 +642,7 @@ func SlackHooksEditPost(ctx *context.Context, form auth.NewSlackHookForm) {
if form.HasInvalidChannel() {
ctx.Flash.Error(ctx.Tr("repo.settings.add_webhook.invalid_channel_name"))
ctx.Redirect(fmt.Sprintf("%s/settings/hooks/%d", orCtx.Link, w.ID))
ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID))
return
}
+1 -1
View File
@@ -284,7 +284,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
return nil, nil
}
commitsHistory = models.ValidateCommitsWithEmails(commitsHistory)
commitsHistory = models.ParseCommitsWithSignature(commitsHistory)
commitsHistory = models.ParseCommitsWithSignature(commitsHistory, ctx.Repo.Repository)
ctx.Data["Commits"] = commitsHistory
+14 -8
View File
@@ -399,7 +399,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/recover_account", user.ResetPasswdPost)
m.Get("/forgot_password", user.ForgotPasswd)
m.Post("/forgot_password", user.ForgotPasswdPost)
m.Get("/logout", user.SignOut)
m.Post("/logout", user.SignOut)
})
// ***** END: User *****
@@ -408,6 +408,7 @@ func RegisterRoutes(m *macaron.Macaron) {
// ***** START: Admin *****
m.Group("/admin", func() {
m.Get("", adminReq, admin.Dashboard)
m.Post("", adminReq, bindIgnErr(auth.AdminDashboardForm{}), admin.DashboardPost)
m.Get("/config", admin.Config)
m.Post("/config/test_mail", admin.SendTestMail)
m.Group("/monitor", func() {
@@ -428,6 +429,11 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/:userid/delete", admin.DeleteUser)
})
m.Group("/emails", func() {
m.Get("", admin.Emails)
m.Post("/activate", admin.ActivateEmail)
})
m.Group("/orgs", func() {
m.Get("", admin.Organizations)
})
@@ -469,7 +475,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/notices", func() {
m.Get("", admin.Notices)
m.Post("/delete", admin.DeleteNotices)
m.Get("/empty", admin.EmptyNotices)
m.Post("/empty", admin.EmptyNotices)
})
}, adminReq)
// ***** END: Admin *****
@@ -485,7 +491,7 @@ func RegisterRoutes(m *macaron.Macaron) {
}, reqSignIn)
m.Group("/:username", func() {
m.Get("/action/:action", user.Action)
m.Post("/action/:action", user.Action)
}, reqSignIn)
if macaron.Env == macaron.DEV {
@@ -517,7 +523,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/^:type(issues|pulls)$", user.Issues)
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
m.Get("/members", org.Members)
m.Get("/members/action/:action", org.MembersAction)
m.Post("/members/action/:action", org.MembersAction)
m.Get("/teams", org.Teams)
}, context.OrgAssignment(true))
@@ -525,8 +531,8 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/:org", func() {
m.Get("/teams/:team", org.TeamMembers)
m.Get("/teams/:team/repositories", org.TeamRepositories)
m.Route("/teams/:team/action/:action", "GET,POST", org.TeamsAction)
m.Route("/teams/:team/action/repo/:action", "GET,POST", org.TeamsRepoAction)
m.Post("/teams/:team/action/:action", org.TeamsAction)
m.Post("/teams/:team/action/repo/:action", org.TeamsRepoAction)
}, context.OrgAssignment(true, false, true))
m.Group("/:org", func() {
@@ -660,7 +666,7 @@ func RegisterRoutes(m *macaron.Macaron) {
})
}, reqSignIn, context.RepoAssignment(), context.UnitTypes(), reqRepoAdmin, context.RepoRef())
m.Get("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), context.UnitTypes(), repo.Action)
m.Post("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), context.UnitTypes(), repo.Action)
m.Group("/:username/:reponame", func() {
m.Group("/issues", func() {
@@ -714,7 +720,7 @@ func RegisterRoutes(m *macaron.Macaron) {
Post(bindIgnErr(auth.CreateMilestoneForm{}), repo.NewMilestonePost)
m.Get("/:id/edit", repo.EditMilestone)
m.Post("/:id/edit", bindIgnErr(auth.CreateMilestoneForm{}), repo.EditMilestonePost)
m.Get("/:id/:action", repo.ChangeMilestonStatus)
m.Post("/:id/:action", repo.ChangeMilestonStatus)
m.Post("/delete", repo.DeleteMilestone)
}, context.RepoMustNotBeArchived(), reqRepoIssuesOrPullsWriter, context.RepoRef())
m.Group("/milestone", func() {
+15 -1
View File
@@ -928,6 +928,7 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au
LoginName: gothUser.(goth.User).UserID,
}
//nolint: dupl
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
@@ -942,6 +943,9 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplLinkAccount, &form)
case models.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplLinkAccount, &form)
default:
ctx.ServerError("CreateUser", err)
}
@@ -1213,9 +1217,19 @@ func ActivateEmail(ctx *context.Context) {
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
if u, err := models.GetUserByID(email.UID); err != nil {
log.Warn("GetUserByID: %d", email.UID)
} else {
// Allow user to validate more emails
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
}
}
ctx.Redirect(setting.AppSubURL + "/user/settings/email")
// FIXME: e-mail verification does not require the user to be logged in,
// so this could be redirecting to the login page.
// Should users be logged in automatically here? (consider 2FA requirements, etc.)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}
// ForgotPasswd render the forget pasword page
+4
View File
@@ -400,6 +400,7 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si
Passwd: password,
IsActive: !setting.Service.RegisterEmailConfirm,
}
//nolint: dupl
if err := models.CreateUser(u); err != nil {
switch {
case models.IsErrUserAlreadyExist(err):
@@ -414,6 +415,9 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si
case models.IsErrNamePatternNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSignUpOID, &form)
case models.IsErrNameCharsNotAllowed(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplSignUpOID, &form)
default:
ctx.ServerError("CreateUser", err)
}
+60 -2
View File
@@ -88,6 +88,51 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Send activation Email
if ctx.Query("_method") == "SENDACTIVATION" {
var address string
if ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName) {
log.Error("Send activation: activation still pending")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if ctx.Query("id") == "PRIMARY" {
if ctx.User.IsActive {
log.Error("Send activation: email not set for activation")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
mailer.SendActivateAccountMail(ctx.Locale, ctx.User)
address = ctx.User.Email
} else {
id := ctx.QueryInt64("id")
email, err := models.GetEmailAddressByID(ctx.User.ID, id)
if err != nil {
log.Error("GetEmailAddressByID(%d,%d) error: %v", ctx.User.ID, id, err)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email == nil {
log.Error("Send activation: EmailAddress not found; user:%d, id: %d", ctx.User.ID, id)
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
if email.IsActivated {
log.Error("Send activation: email not set for activation")
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
address = email.Email
}
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", address, timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language())))
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
return
}
// Set Email Notification Preference
if ctx.Query("_method") == "NOTIFICATION" {
preference := ctx.Query("preference")
@@ -134,7 +179,6 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) {
// Send confirmation email
if setting.Service.RegisterEmailConfirm {
mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err)
}
@@ -223,11 +267,25 @@ func UpdateUIThemePost(ctx *context.Context, form auth.UpdateThemeForm) {
}
func loadAccountData(ctx *context.Context) {
emails, err := models.GetEmailAddresses(ctx.User.ID)
emlist, err := models.GetEmailAddresses(ctx.User.ID)
if err != nil {
ctx.ServerError("GetEmailAddresses", err)
return
}
type UserEmail struct {
models.EmailAddress
CanBePrimary bool
}
pendingActivation := ctx.Cache.IsExist("MailResendLimit_" + ctx.User.LowerName)
emails := make([]*UserEmail, len(emlist))
for i, em := range emlist {
var email UserEmail
email.EmailAddress = *em
email.CanBePrimary = em.IsActivated
emails[i] = &email
}
ctx.Data["Emails"] = emails
ctx.Data["EmailNotificationsPreference"] = ctx.User.EmailNotifications()
ctx.Data["ActivationsPending"] = pendingActivation
ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm
}
+3
View File
@@ -58,6 +58,9 @@ func handleUsernameChange(ctx *context.Context, newName string) {
case models.IsErrNamePatternNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
ctx.Redirect(setting.AppSubURL + "/user/settings")
case models.IsErrNameCharsNotAllowed(err):
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
ctx.Redirect(setting.AppSubURL + "/user/settings")
default:
ctx.ServerError("ChangeUserName", err)
}
+12
View File
@@ -5,6 +5,7 @@
package utils
import (
"html"
"strings"
)
@@ -34,3 +35,14 @@ func IsValidSlackChannel(channelName string) bool {
return true
}
// SanitizeFlashErrorString will sanitize a flash error string
func SanitizeFlashErrorString(x string) string {
runes := []rune(x)
if len(runes) > 512 {
x = "..." + string(runes[len(runes)-512:])
}
return strings.Replace(html.EscapeString(x), "\n", "<br>", -1)
}
+15
View File
@@ -61,3 +61,18 @@ func RemoveLabel(issue *models.Issue, doer *models.User, label *models.Label) er
notification.NotifyIssueChangeLabels(doer, issue, nil, []*models.Label{label})
return nil
}
// ReplaceLabels removes all current labels and add new labels to the issue.
func ReplaceLabels(issue *models.Issue, doer *models.User, labels []*models.Label) error {
old, err := models.GetLabelsByIssueID(issue.ID)
if err != nil {
return err
}
if err := issue.ReplaceLabels(labels, doer); err != nil {
return err
}
notification.NotifyIssueChangeLabels(doer, issue, labels, old)
return nil
}
+9 -1
View File
@@ -62,7 +62,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) e
unfiltered = append(unfiltered, ids...)
// =========== Issue watchers ===========
ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID)
ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID, true)
if err != nil {
return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
}
@@ -80,6 +80,14 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) e
// Avoid mailing the doer
visited[ctx.Doer.ID] = true
// Avoid mailing explicit unwatched
ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID, false)
if err != nil {
return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
}
for _, i := range ids {
visited[i] = true
}
if err = mailIssueCommentBatch(ctx, unfiltered, visited, false); err != nil {
return fmt.Errorf("mailIssueCommentBatch(): %v", err)
+1 -1
View File
@@ -47,7 +47,7 @@ func checkAndUpdateStatus(pr *models.PullRequest) {
// Make sure there is no waiting test to process before leaving the checking status.
if !pullRequestQueue.Exist(pr.ID) {
if err := pr.UpdateCols("status, conflicted_files"); err != nil {
if err := pr.UpdateCols("merge_base", "status", "conflicted_files"); err != nil {
log.Error("Update[%d]: %v", pr.ID, err)
}
}
+6 -1
View File
@@ -23,7 +23,7 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*models.CommitStatus, re
return structs.CommitStatusSuccess
}
var returnedStatus = structs.CommitStatusPending
var returnedStatus = structs.CommitStatusSuccess
for _, ctx := range requiredContexts {
var targetStatus structs.CommitStatusState
for _, commitStatus := range commitStatuses {
@@ -91,6 +91,11 @@ func IsPullCommitStatusPass(pr *models.PullRequest) (bool, error) {
// GetPullRequestCommitStatusState returns pull request merged commit status state
func GetPullRequestCommitStatusState(pr *models.PullRequest) (structs.CommitStatusState, error) {
// Ensure HeadRepo is loaded
if err := pr.LoadHeadRepo(); err != nil {
return "", errors.Wrap(err, "LoadHeadRepo")
}
// check if all required status checks are successful
headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
if err != nil {
+9
View File
@@ -335,6 +335,15 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
StdErr: errbuf.String(),
Err: err,
}
} else if strings.Contains(errbuf.String(), "! [remote rejected]") {
err := models.ErrPushRejected{
Style: mergeStyle,
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
err.GenerateMessage()
return err
}
return fmt.Errorf("git push: %s", errbuf.String())
}
+8
View File
@@ -231,6 +231,10 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) {
}
}()
if err := pr.LoadHeadRepo(); err != nil {
log.Error("Unable to load head repository for PR[%d] Error: %v", pr.ID, err)
return err
}
headRepoPath := pr.HeadRepo.RepoPath()
if err := git.Clone(headRepoPath, tmpBasePath, git.CloneRepoOptions{
@@ -247,6 +251,10 @@ func PushToBaseRepo(pr *models.PullRequest) (err error) {
return fmt.Errorf("OpenRepository: %v", err)
}
if err := pr.LoadBaseRepo(); err != nil {
log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err)
return err
}
if err := gitRepo.AddRemote("base", pr.BaseRepo.RepoPath(), false); err != nil {
return fmt.Errorf("tmpGitRepo.AddRemote: %v", err)
}
+47 -44
View File
@@ -15,50 +15,53 @@
{{.i18n.Tr "admin.dashboard.operations"}}
</h4>
<div class="ui attached table segment">
<table class="ui very basic table">
<tbody>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_inactivate_accounts"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=1">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_repo_archives"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=2">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_missing_repos"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=3">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.git_gc_repos"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=4">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=5">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.resync_all_hooks"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=6">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=7">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.sync_external_users"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=8">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=9">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td>
<td><i class="fa fa-caret-square-o-right"></i> <a href="{{AppSubUrl}}/admin?op=10">{{.i18n.Tr "admin.dashboard.operation_run"}}</a></td>
</tr>
</tbody>
</table>
<form method="post" action="{{AppSubUrl}}/admin">
{{.CsrfTokenHtml}}
<table class="ui very basic table">
<tbody>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_inactivate_accounts"}}</td>
<td><button type="submit" class="ui green button" name="op" value="1"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_repo_archives"}}</td>
<td><button type="submit" class="ui green button" name="op" value="2"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_missing_repos"}}</td>
<td><button type="submit" class="ui green button" name="op" value="3"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.git_gc_repos"}}</td>
<td><button type="submit" class="ui green button" name="op" value="4"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.resync_all_sshkeys"}}</td>
<td><button type="submit" class="ui green button" name="op" value="5"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.resync_all_hooks"}}</td>
<td><button type="submit" class="ui green button" name="op" value="6"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.reinit_missing_repos"}}</td>
<td><button type="submit" class="ui green button" name="op" value="7"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.sync_external_users"}}</td>
<td><button type="submit" class="ui green button" name="op" value="8"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.git_fsck"}}</td>
<td><button type="submit" class="ui green button" name="op" value="9"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
<tr>
<td>{{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td>
<td><button type="submit" class="ui green button" name="op" value="10"><i class="fa fa-caret-square-o-right"></i> {{.i18n.Tr "admin.dashboard.operation_run"}}</button></td>
</tr>
</tbody>
</table>
</form>
</div>
<h4 class="ui top attached header">
+101
View File
@@ -0,0 +1,101 @@
{{template "base/head" .}}
<div class="admin user">
{{template "admin/navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{.i18n.Tr "admin.emails.email_manage_panel"}} ({{.i18n.Tr "admin.total" .Total}})
</h4>
<div class="ui attached segment">
<div class="ui right floated secondary filter menu">
<!-- Sort -->
<div class="ui dropdown type jump item">
<span class="text">
{{.i18n.Tr "repo.issues.filter_sort"}}
<i class="dropdown icon"></i>
</span>
<div class="menu">
<a class="{{if or (eq .SortType "email") (not .SortType)}}active{{end}} item" href="{{$.Link}}?sort=email&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.email"}}</a>
<a class="{{if eq .SortType "reverseemail"}}active{{end}} item" href="{{$.Link}}?sort=reverseemail&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.email_reverse"}}</a>
<a class="{{if eq .SortType "username"}}active{{end}} item" href="{{$.Link}}?sort=username&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.name"}}</a>
<a class="{{if eq .SortType "reverseusername"}}active{{end}} item" href="{{$.Link}}?sort=reverseusername&q={{$.Keyword}}">{{.i18n.Tr "admin.emails.filter_sort.name_reverse"}}</a>
</div>
</div>
</div>
<form class="ui form ignore-dirty" style="max-width: 90%">
<div class="ui fluid action input">
<input name="q" value="{{.Keyword}}" placeholder="{{.i18n.Tr "explore.search"}}..." autofocus>
<button class="ui blue button">{{.i18n.Tr "explore.search"}}</button>
</div>
</form>
</div>
<div class="ui attached table segment">
<table class="ui very basic striped table">
<thead>
<tr>
<th>{{.i18n.Tr "admin.users.name"}}</th>
<th>{{.i18n.Tr "admin.users.full_name"}}</th>
<th>{{.i18n.Tr "email"}}</th>
<th>{{.i18n.Tr "admin.emails.primary"}}</th>
<th>{{.i18n.Tr "admin.emails.activated"}}</th>
</tr>
</thead>
<tbody>
{{range .Emails}}
<tr>
<td><a href="{{AppSubUrl}}/{{.Name}}">{{.Name}}</a></td>
<td><span class="text truncate">{{.FullName}}</span></td>
<td><span class="text email">{{.Email}}</span></td>
<td><i class="fa fa{{if .IsPrimary}}-check{{end}}-square-o"></i></td>
<td>
{{if .CanChange}}
<a class="link-email-action" href data-uid="{{.UID}}"
data-email="{{.Email}}"
data-primary="{{if .IsPrimary}}1{{else}}0{{end}}"
data-activate="{{if .IsActivated}}0{{else}}1{{end}}">
<i class="fa fa{{if .IsActivated}}-check{{end}}-square-o"></i>
</a>
{{else}}
<i class="fa fa{{if .IsActivated}}-check{{end}}-square-o"></i>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "base/paginate" .}}
<div class="ui basic modal" id="change-email-modal">
<div class="ui icon header">
{{.i18n.Tr "admin.emails.change_email_header"}}
</div>
<div class="content center">
<p>{{.i18n.Tr "admin.emails.change_email_text"}}</p>
<form class="ui form" id="email-action-form" action="{{AppSubUrl}}/admin/emails/activate" method="post">
{{$.CsrfTokenHtml}}
<input type="hidden" id="query-sort" name="sort" value="{{.SortType}}">
<input type="hidden" id="query-keyword" name="q" value="{{.Keyword}}">
<input type="hidden" id="query-primary" name="is_primary" value="{{.IsPrimary}}" required>
<input type="hidden" id="query-activated" name="is_activated" value="{{.IsActivated}}" required>
<input type="hidden" id="form-uid" name="uid" value="" required>
<input type="hidden" id="form-email" name="email" value="" required>
<input type="hidden" id="form-primary" name="primary" value="" required>
<input type="hidden" id="form-activate" name="activate" value="" required>
<div class="center actions">
<div class="ui basic cancel inverted button">{{$.i18n.Tr "settings.cancel"}}</div>
<button class="ui basic inverted yellow button">{{$.i18n.Tr "modal.yes"}}</button>
</div>
</form>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}
+1
View File
@@ -8,6 +8,7 @@
<li {{if .PageIsAdminRepositories}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/repos">{{.i18n.Tr "admin.repositories"}}</a></li>
<li {{if .PageIsAdminHooks}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/hooks">{{.i18n.Tr "admin.hooks"}}</a></li>
<li {{if .PageIsAdminAuthentications}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/auths">{{.i18n.Tr "admin.authentication"}}</a></li>
<li {{if .PageIsAdminEmails}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/emails">{{.i18n.Tr "admin.emails"}}</a></li>
<li {{if .PageIsAdminConfig}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/config">{{.i18n.Tr "admin.config"}}</a></li>
<li {{if .PageIsAdminNotices}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/notices">{{.i18n.Tr "admin.notices"}}</a></li>
<li {{if .PageIsAdminMonitor}}class="current"{{end}}><a href="{{AppSubUrl}}/admin/monitor">{{.i18n.Tr "admin.monitor"}}</a></li>
+3
View File
@@ -17,6 +17,9 @@
<a class="{{if .PageIsAdminAuthentications}}active{{end}} item" href="{{AppSubUrl}}/admin/auths">
{{.i18n.Tr "admin.authentication"}}
</a>
<a class="{{if .PageIsAdminEmails}}active{{end}} item" href="{{AppSubUrl}}/admin/emails">
{{.i18n.Tr "admin.emails"}}
</a>
<a class="{{if .PageIsAdminConfig}}active{{end}} item" href="{{AppSubUrl}}/admin/config">
{{.i18n.Tr "admin.config"}}
</a>
+4 -1
View File
@@ -40,7 +40,10 @@
<th></th>
<th colspan="5">
<div class="ui right">
<a class="ui red small button" href="{{AppSubUrl}}/admin/notices/empty">{{.i18n.Tr "admin.notices.delete_all"}}</a>
<form method="post" action="{{AppSubUrl}}/admin/notices/empty">
{{.CsrfTokenHtml}}
<button type="submit" class="ui red small button">{{.i18n.Tr "admin.notices.delete_all"}}</button>
</form>
</div>
<div class="ui floating upward dropdown small button">
<span class="text">{{.i18n.Tr "admin.notices.actions"}}</span>
+1 -1
View File
@@ -156,7 +156,7 @@
{{else}}
<meta property="og:title" content="{{AppName}}">
<meta property="og:type" content="website" />
<meta property="og:image" content="{{StaticUrlPrefix}}img/gitea-lg.png" />
<meta property="og:image" content="{{StaticUrlPrefix}}/img/gitea-lg.png" />
<meta property="og:url" content="{{AppUrl}}" />
<meta property="og:description" content="{{MetaDescription}}">
{{end}}
+1 -1
View File
@@ -109,7 +109,7 @@
{{end}}
<div class="divider"></div>
<a class="item" href="{{AppSubUrl}}/user/logout">
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout" data-redirect="{{AppSubUrl}}/">
<i class="octicon octicon-sign-out"></i>
{{.i18n.Tr "sign_out"}}<!-- Sign Out -->
</a>
+10 -4
View File
@@ -22,10 +22,10 @@
{{ $isPublic := index $.MembersIsPublicMember .ID}}
{{if $isPublic}}
<strong>{{$.i18n.Tr "org.members.public"}}</strong>
{{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}}(<a href="{{$.OrgLink}}/members/action/private?uid={{.ID}}">{{$.i18n.Tr "org.members.public_helper"}}</a>){{end}}
{{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}}(<a class="link-action" href data-url="{{$.OrgLink}}/members/action/private?uid={{.ID}}">{{$.i18n.Tr "org.members.public_helper"}}</a>){{end}}
{{else}}
<strong>{{$.i18n.Tr "org.members.private"}}</strong>
{{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}}(<a href="{{$.OrgLink}}/members/action/public?uid={{.ID}}">{{$.i18n.Tr "org.members.private_helper"}}</a>){{end}}
{{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}}(<a class="link-action" href data-url="{{$.OrgLink}}/members/action/public?uid={{.ID}}">{{$.i18n.Tr "org.members.private_helper"}}</a>){{end}}
{{end}}
</div>
</div>
@@ -48,9 +48,15 @@
<div class="ui four wide column">
<div class="text right">
{{if eq $.SignedUser.ID .ID}}
<a class="ui red small button" href="{{$.OrgLink}}/members/action/leave?uid={{.ID}}">{{$.i18n.Tr "org.members.leave"}}</a>
<form method="post" action="{{$.OrgLink}}/members/action/leave">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button" name="uid" value="{{.ID}}">{{$.i18n.Tr "org.members.leave"}}</button>
</form>
{{else if $.IsOrganizationOwner}}
<a class="ui red small button" href="{{$.OrgLink}}/members/action/remove?uid={{.ID}}">{{$.i18n.Tr "org.members.remove"}}</a>
<form method="post" action="{{$.OrgLink}}/members/action/remove">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button" name="uid" value="{{.ID}}">{{$.i18n.Tr "org.members.remove"}}</button>
</form>
{{end}}
</div>
</div>
+4 -1
View File
@@ -27,7 +27,10 @@
{{range .Team.Members}}
<div class="item">
{{if $.IsOrganizationOwner}}
<a class="ui red small button right" href="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/remove?uid={{.ID}}">{{$.i18n.Tr "org.members.remove"}}</a>
<form method="post" action="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/remove">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button right" name="uid" value="{{.ID}}">{{$.i18n.Tr "org.members.remove"}}</button>
</form>
{{end}}
<a href="{{.HomeLink}}">
<img class="ui avatar image" src="{{.RelAvatarLink}}">
+4 -1
View File
@@ -35,7 +35,10 @@
{{range .Team.Repos}}
<div class="item">
{{if $canAddRemove}}
<a class="ui red small button right" href="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/remove?repoid={{.ID}}">{{$.i18n.Tr "remove"}}</a>
<form method="post" action="{{$.OrgLink}}/teams/{{$.Team.LowerName}}/action/repo/remove">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button right" name="repoid" value="{{.ID}}">{{$.i18n.Tr "remove"}}</button>
</form>
{{end}}
<a class="member" href="{{AppSubUrl}}/{{$.Org.Name}}/{{.Name}}">
<i class="octicon octicon-{{if .IsPrivate}}lock{{else if .IsFork}}repo-forked{{else if .IsMirror}}repo-clone{{else}}repo{{end}}"></i>
+10 -2
View File
@@ -3,9 +3,17 @@
<strong>{{.Team.Name}}</strong>
<div class="ui right">
{{if .Team.IsMember $.SignedUser.ID}}
<a class="ui red tiny button" href="{{.OrgLink}}/teams/{{.Team.LowerName}}/action/leave?uid={{$.SignedUser.ID}}&page=home">{{$.i18n.Tr "org.teams.leave"}}</a>
<form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName}}/action/leave">
{{$.CsrfTokenHtml}}
<input type="hidden" name="page" value="home"/>
<button type="submit" class="ui red tiny button" name="uid" value="{{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.leave"}}</button>
</form>
{{else if .IsOrganizationOwner}}
<a class="ui blue tiny button" href="{{.OrgLink}}/teams/{{.Team.LowerName}}/action/join?uid={{$.SignedUser.ID}}&page=team">{{$.i18n.Tr "org.teams.join"}}</a>
<form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName}}/action/join">
{{$.CsrfTokenHtml}}
<input type="hidden" name="page" value="team"/>
<button type="submit" class="ui blue tiny button" name="uid" value="{{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.join"}}</button>
</form>
{{end}}
</div>
</h4>
+8 -2
View File
@@ -17,9 +17,15 @@
<a class="text black" href="{{$.OrgLink}}/teams/{{.LowerName}}"><strong>{{.Name}}</strong></a>
<div class="ui right">
{{if .IsMember $.SignedUser.ID}}
<a class="ui red small button" href="{{$.OrgLink}}/teams/{{.LowerName}}/action/leave?uid={{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.leave"}}</a>
<form method="post" action="{{$.OrgLink}}/teams/{{.LowerName}}/action/leave">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui red small button" name="uid" value="{{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.leave"}}</button>
</form>
{{else if $.IsOrganizationOwner}}
<a class="ui blue small button" href="{{$.OrgLink}}/teams/{{.LowerName}}/action/join?uid={{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.join"}}</a>
<form method="post" action="{{$.OrgLink}}/teams/{{.LowerName}}/action/join">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui blue small button" name="uid" value="{{$.SignedUser.ID}}">{{$.i18n.Tr "org.teams.join"}}</button>
</form>
{{end}}
</div>
</div>
+45 -28
View File
@@ -2,25 +2,40 @@
<div class="repository diff">
{{template "repo/header" .}}
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}">
<div class="ui top attached info clearing segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
{{$class := ""}}
{{if .Commit.Signature}}
{{$class = (printf "%s%s" $class " isSigned")}}
{{if .Verification.Verified}}
{{if eq .Verification.TrustStatus "trusted"}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{else if eq .Verification.TrustStatus "untrusted"}}
{{$class = (printf "%s%s" $class " isVerifiedUntrusted")}}
{{else}}
{{$class = (printf "%s%s" $class " isVerifiedUnmatched")}}
{{end}}
{{else if .Verification.Warning}}
{{$class = (printf "%s%s" $class " isWarning")}}
{{end}}
{{end}}
<div class="ui top attached info clearing segment {{$class}}">
<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}">
{{.i18n.Tr "repo.diff.browse_source"}}
</a>
<h3 class="has-emoji">{{RenderCommitMessage .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_status" .CommitStatus}}</h3>
<h3 class="has-emoji"><span class="message-wrapper"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</span></span>{{template "repo/commit_status" .CommitStatus}}</h3>
{{if IsMultilineCommitMessage .Commit.Message}}
<pre class="commit-body">{{RenderCommitBody .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
{{end}}
<span class="text grey"><i class="octicon octicon-git-branch"></i>{{.BranchName}}</span>
</div>
<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
<div class="ui attached info segment {{$class}}">
<div class="ui stackable grid">
<div class="nine wide column">
{{if .Author}}
<img class="ui avatar image" src="{{.Author.RelAvatarLink}}" />
{{if .Author.FullName}}
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a> {{if .IsSigned}}<{{.Commit.Author.Email}}>{{end}}
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong> {{if .IsSigned}}&lt;{{.Commit.Author.Email}}&gt;{{end}}</a>
{{else}}
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a> {{if .IsSigned}}<{{.Commit.Author.Email}}>{{end}}
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong> {{if .IsSigned}}&lt;{{.Commit.Author.Email}}&gt;{{end}}</a>
{{end}}
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Commit.Author.Email}}" />
@@ -30,7 +45,7 @@
<span> </span>
{{if ne .Verification.CommittingUser.ID 0}}
<img class="ui avatar image" src="{{.Verification.CommittingUser.RelAvatarLink}}" />
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong> &lt;{{.Commit.Committer.Email}}&gt;</a>
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Commit.Committer.Email}}" />
<strong>{{.Commit.Committer.Name}}</strong>
@@ -58,40 +73,42 @@
</div><!-- end grid -->
</div>
{{if .Commit.Signature}}
{{if .Verification.Verified }}
<div class="ui bottom attached positive message">
<div class="ui bottom attached message {{$class}}">
{{if .Verification.Verified }}
{{if ne .Verification.SigningUser.ID 0}}
<i class="green lock icon"></i>
<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<i class="lock icon"></i>
{{if eq .Verification.TrustStatus "trusted"}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by"}}:</span>
{{else if eq .Verification.TrustStatus "untrusted"}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
{{else}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
{{end}}
<img class="ui avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong></a> <{{.Verification.SigningEmail}}>
<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}></a>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
{{else}}
<i class="icons" title="{{.i18n.Tr "gpg.default_key"}}">
<i class="green lock icon"></i>
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<img class="ui avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
<strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}>
<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
{{end}}
</div>
{{else if .Verification.Warning}}
<div class="ui bottom attached message">
<i class="red unlock icon"></i>
<span class="red text">{{.i18n.Tr .Verification.Reason}}</span>
<span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
</div>
{{else}}
<div class="ui bottom attached message">
<i class="grey unlock icon"></i>
{{else if .Verification.Warning}}
<i class="unlock icon"></i>
<span class="ui text">{{.i18n.Tr .Verification.Reason}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
{{else}}
<i class="unlock icon"></i>
{{.i18n.Tr .Verification.Reason}}
{{if and .Verification.SigningKey (ne .Verification.SigningKey.KeyID "")}}
<span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
{{end}}
</div>
{{end}}
{{end}}
</div>
{{end}}
{{if .Note}}
<div class="ui top attached info segment message git-notes">
+20 -12
View File
@@ -28,7 +28,13 @@
{{if .Signature}}
{{$class = (printf "%s%s" $class " isSigned")}}
{{if .Verification.Verified}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{if eq .Verification.TrustStatus "trusted"}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{else if eq .Verification.TrustStatus "untrusted"}}
{{$class = (printf "%s%s" $class " isVerifiedUntrusted")}}
{{else}}
{{$class = (printf "%s%s" $class " isVerifiedUnmatched")}}
{{end}}
{{else if .Verification.Warning}}
{{$class = (printf "%s%s" $class " isWarning")}}
{{end}}
@@ -38,20 +44,22 @@
{{else}}
<span class="{{$class}}">
{{end}}
{{ShortSha .ID.String}}
<span class="shortsha">{{ShortSha .ID.String}}</span>
{{if .Signature}}
<div class="ui detail icon button">
{{if .Verification.Verified}}
{{if ne .Verification.SigningUser.ID 0}}
<i title="{{.Verification.Reason}}" class="lock green icon"></i>
{{else}}
<i title="{{.Verification.Reason}}" class="icons">
<i class="green lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
{{end}}
{{else if .Verification.Warning}}
<i title="{{$.i18n.Tr .Verification.Reason}}" class="red unlock icon"></i>
<div title="{{if eq .Verification.TrustStatus "trusted"}}{{else if eq .Verification.TrustStatus "untrusted"}}{{$.i18n.Tr "repo.commits.signed_by_untrusted_user"}}: {{else}}{{$.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: {{end}}{{.Verification.Reason}}">
{{if ne .Verification.SigningUser.ID 0}}
<i class="lock icon"></i>
<img class="ui signature avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
{{else}}
<i title="{{.Verification.Reason}}" class="icons">
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<img class="ui signature avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
{{end}}
</div>
{{else}}
<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
{{end}}
+22 -16
View File
@@ -20,22 +20,28 @@
</div>
{{if not .IsBeingCreated}}
<div class="repo-buttons">
<div class="ui labeled button" tabindex="0">
<a class="ui compact basic button" href="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch?redirect_to={{$.Link}}">
<i class="icon fa-eye{{if not $.IsWatchingRepo}}-slash{{end}}"></i>{{if $.IsWatchingRepo}}{{$.i18n.Tr "repo.unwatch"}}{{else}}{{$.i18n.Tr "repo.watch"}}{{end}}
</a>
<a class="ui basic label" href="{{.Link}}/watchers">
{{.NumWatches}}
</a>
</div>
<div class="ui labeled button" tabindex="0">
<a class="ui compact basic button" href="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star?redirect_to={{$.Link}}">
<i class="icon star{{if not $.IsStaringRepo}} outline{{end}}"></i>{{if $.IsStaringRepo}}{{$.i18n.Tr "repo.unstar"}}{{else}}{{$.i18n.Tr "repo.star"}}{{end}}
</a>
<a class="ui basic label" href="{{.Link}}/stars">
{{.NumStars}}
</a>
</div>
<form method="post" action="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}un{{end}}watch?redirect_to={{$.Link}}">
{{$.CsrfTokenHtml}}
<div class="ui labeled button" tabindex="0">
<button type="submit" class="ui compact basic button">
<i class="icon fa-eye{{if not $.IsWatchingRepo}}-slash{{end}}"></i>{{if $.IsWatchingRepo}}{{$.i18n.Tr "repo.unwatch"}}{{else}}{{$.i18n.Tr "repo.watch"}}{{end}}
</button>
<a class="ui basic label" href="{{.Link}}/watchers">
{{.NumWatches}}
</a>
</div>
</form>
<form method="post" action="{{$.RepoLink}}/action/{{if $.IsStaringRepo}}un{{end}}star?redirect_to={{$.Link}}">
{{$.CsrfTokenHtml}}
<div class="ui labeled button" tabindex="0">
<button type="submit" class="ui compact basic button">
<i class="icon star{{if not $.IsStaringRepo}} outline{{end}}"></i>{{if $.IsStaringRepo}}{{$.i18n.Tr "repo.unstar"}}{{else}}{{$.i18n.Tr "repo.star"}}{{end}}
</button>
<a class="ui basic label" href="{{.Link}}/stars">
{{.NumStars}}
</a>
</div>
</form>
{{if and (not .IsEmpty) ($.Permission.CanRead $.UnitTypeCode)}}
<div class="ui labeled button {{if and ($.IsSigned) (not $.CanSignedUserFork)}}disabled-repo-button{{end}}" tabindex="0">
<a class="ui compact basic button {{if or (not $.IsSigned) (not $.CanSignedUserFork)}}poping up{{end}}" {{if $.CanSignedUserFork}}href="{{AppSubUrl}}/repo/fork/{{.ID}}"{{else if $.IsSigned}} data-content="{{$.i18n.Tr "repo.fork_from_self"}}" {{ else }} data-content="{{$.i18n.Tr "repo.fork_guest_user" }}" rel="nofollow" href="{{AppSubUrl}}/user/login?redirect_to={{AppSubUrl}}/repo/fork/{{.ID}}" {{end}} data-position="top center" data-variation="tiny">
+2 -2
View File
@@ -71,9 +71,9 @@
<div class="ui right operate">
<a href="{{$.Link}}/{{.ID}}/edit" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-pencil"></i> {{$.i18n.Tr "repo.issues.label_edit"}}</a>
{{if .IsClosed}}
<a href="{{$.Link}}/{{.ID}}/open" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-check"></i> {{$.i18n.Tr "repo.milestones.open"}}</a>
<a class="link-action" href data-url="{{$.Link}}/{{.ID}}/open" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-check"></i> {{$.i18n.Tr "repo.milestones.open"}}</a>
{{else}}
<a href="{{$.Link}}/{{.ID}}/close" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-x"></i> {{$.i18n.Tr "repo.milestones.close"}}</a>
<a class="link-action" href data-url="{{$.Link}}/{{.ID}}/close" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-x"></i> {{$.i18n.Tr "repo.milestones.close"}}</a>
{{end}}
<a class="delete-button" href="#" data-url="{{$.RepoLink}}/milestones/delete" data-id="{{.ID}}"><i class="octicon octicon-trashcan"></i> {{$.i18n.Tr "repo.issues.label_delete"}}</a>
</div>
@@ -10,7 +10,7 @@
<div class="item context clipboard" data-clipboard-text="{{Printf "%s%s/issues/%d#%s" AppUrl .ctx.Repository.FullName .ctx.Issue.Index .item.HashTag}}">{{.ctx.i18n.Tr "repo.issues.context.copy_link"}}</div>
{{end}}
<div class="item context quote-reply {{if .diff}}quote-reply-diff{{end}}" data-target="{{.item.ID}}">{{.ctx.i18n.Tr "repo.issues.context.quote_reply"}}</div>
{{if or .ctx.Permission.IsAdmin (eq .item.Poster.ID .ctx.SignedUserID)}}
{{if or .ctx.Permission.IsAdmin .ctx.IsIssuePoster .ctx.IsIssueWriter}}
<div class="divider"></div>
<div class="item context edit-content">{{.ctx.i18n.Tr "repo.issues.context.edit"}}</div>
{{if .delete}}
+23 -12
View File
@@ -15,21 +15,32 @@
<strong>{{.LatestCommit.Author.Name}}</strong>
{{end}}
{{end}}
<a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified }} isVerified {{end}}{{end}}" href="{{.RepoLink}}/commit/{{.LatestCommit.ID}}">
{{ShortSha .LatestCommit.ID.String}}
{{if .LatestCommit.Signature}}
<div class="ui detail icon button">
{{if .LatestCommitVerification.Verified}}
<i title="{{.LatestCommitVerification.Reason}}" class="lock green icon"></i>
{{else}}
<i title="{{$.i18n.Tr .LatestCommitVerification.Reason}}" class="unlock icon"></i>
{{end}}
</div>
{{end}}
<a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified }} isVerified{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}Untrusted{{else}}Unmatched{{end}}{{else if .LatestCommitVerification.Warning}} isWarning{{end}}{{end}}" href="{{.RepoLink}}/commit/{{.LatestCommit.ID}}">
<span class="shortsha">{{ShortSha .LatestCommit.ID.String}}</span>
{{if .LatestCommit.Signature}}
<div class="ui detail icon button">
{{if .LatestCommitVerification.Verified}}
<div title="{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}{{.i18n.Tr "repo.commits.signed_by_untrusted_user"}}: {{else}}{{.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: {{end}}{{.LatestCommitVerification.Reason}}">
{{if ne .LatestCommitVerification.SigningUser.ID 0}}
<i class="lock icon"></i>
<img class="ui signature avatar image" src="{{.LatestCommitVerification.SigningUser.RelAvatarLink}}" />
{{else}}
<i title="{{.LatestCommitVerification.Reason}}" class="icons">
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<img class="ui signature avatar image" src="{{AvatarLink .LatestCommitVerification.SigningEmail}}" />
{{end}}
</div>
{{else}}
<i title="{{$.i18n.Tr .LatestCommitVerification.Reason}}" class="unlock icon"></i>
{{end}}
</div>
{{end}}
</a>
{{template "repo/commit_status" .LatestCommitStatus}}
{{ $commitLink:= printf "%s/commit/%s" .RepoLink .LatestCommit.ID }}
<span class="grey has-emoji commit-summary" title="{{.LatestCommit.Summary}}">{{RenderCommitMessageLinkSubject .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}
<span class="grey has-emoji commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}</span>
{{if IsMultilineCommitMessage .LatestCommit.Message}}
<button class="basic compact mini ui icon button commit-button"><i class="ellipsis horizontal icon"></i></button>
<pre class="commit-body" style="display: none;">{{RenderCommitBody .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
+8 -2
View File
@@ -65,9 +65,15 @@
{{if and .IsSigned (ne .SignedUserName .Owner.Name)}}
<li class="follow">
{{if .SignedUser.IsFollowing .Owner.ID}}
<a class="ui basic red button" href="{{.Link}}/action/unfollow?redirect_to={{$.Link}}"><i class="octicon octicon-person"></i> {{.i18n.Tr "user.unfollow"}}</a>
<form method="post" action="{{.Link}}/action/unfollow?redirect_to={{$.Link}}">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui basic red button"><i class="octicon octicon-person"></i> {{.i18n.Tr "user.unfollow"}}</button>
</form>
{{else}}
<a class="ui basic green button" href="{{.Link}}/action/follow?redirect_to={{$.Link}}"><i class="octicon octicon-person"></i> {{.i18n.Tr "user.follow"}}</a>
<form method="post" action="{{.Link}}/action/follow?redirect_to={{$.Link}}">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui basic green button"><i class="octicon octicon-person"></i> {{.i18n.Tr "user.follow"}}</button>
</form>
{{end}}
</li>
{{end}}
+25 -4
View File
@@ -76,7 +76,7 @@
{{$.i18n.Tr "settings.delete_email"}}
</button>
</div>
{{if .IsActivated}}
{{if .CanBePrimary}}
<div class="right floated content">
<form action="{{AppSubUrl}}/user/settings/account/email" method="post">
{{$.CsrfTokenHtml}}
@@ -87,9 +87,30 @@
</div>
{{end}}
{{end}}
{{if not .IsActivated}}
<div class="right floated content">
<form action="{{AppSubUrl}}/user/settings/account/email" method="post">
{{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="SENDACTIVATION">
<input name="id" type="hidden" value="{{if .IsPrimary}}PRIMARY{{else}}}.ID{{end}}">
{{if $.ActivationsPending}}
<button disabled class="ui blue tiny button">{{$.i18n.Tr "settings.activations_pending"}}</button>
{{else}}
<button class="ui blue tiny button">{{$.i18n.Tr "settings.activate_email"}}</button>
{{end}}
</form>
</div>
{{end}}
<div class="content">
<strong>{{.Email}}</strong>
{{if .IsPrimary}}<span class="text red">{{$.i18n.Tr "settings.primary"}}</span>{{end}}
{{if .IsPrimary}}
<div class="ui blue label">{{$.i18n.Tr "settings.primary"}}</div>
{{end}}
{{if .IsActivated}}
<div class="ui green label">{{$.i18n.Tr "settings.activated"}}</div>
{{else}}
<div class="ui label">{{$.i18n.Tr "settings.requires_activation"}}</div>
{{end}}
</div>
</div>
{{end}}
@@ -100,9 +121,9 @@
{{.CsrfTokenHtml}}
<div class="required field {{if .Err_Email}}error{{end}}">
<label for="email">{{.i18n.Tr "settings.add_new_email"}}</label>
<input id="email" name="email" type="email" required>
<input id="email" name="email" type="email" required {{if not .CanAddEmails}}disabled{{end}}>
</div>
<button class="ui green button">
<button class="ui green button" {{if not .CanAddEmails}}disabled{{end}}>
{{.i18n.Tr "settings.add_email"}}
</button>
</form>
+4 -1
View File
@@ -17,7 +17,10 @@
{{range .Orgs}}
<div class="item">
<div class="right floated content">
<a class="ui blue small button" href="{{AppSubUrl}}/org/{{.Name}}/members/action/leave?uid={{.ID}}">{{$.i18n.Tr "org.members.leave"}}</a>
<form method="post" action="{{AppSubUrl}}/org/{{.Name}}/members/action/leave">
{{$.CsrfTokenHtml}}
<button type="submit" class="ui blue small button" name="uid" value="{{.ID}}">{{$.i18n.Tr "org.members.leave"}}</button>
</form>
</div>
<img class="ui mini image" src="{{.RelAvatarLink}}">
<div class="content">
+1 -3
View File
@@ -3,9 +3,7 @@ language: go
sudo: false
go:
- 1.7
- 1.8
- 1.9
- "1.9"
- "1.10"
- "1.11"
- "1.12"
+1
View File
@@ -17,6 +17,7 @@ $ go get github.com/markbates/goth
## Supported Providers
* Amazon
* Apple
* Auth0
* Azure AD
* Battle.net
+3
View File
@@ -3,12 +3,15 @@ module github.com/markbates/goth
require (
cloud.google.com/go v0.30.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gorilla/mux v1.6.2
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1
github.com/gorilla/sessions v1.1.1
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da
github.com/lestrrat-go/jwx v0.9.0
github.com/markbates/going v1.0.0
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c
github.com/pkg/errors v0.8.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd
+8
View File
@@ -2,6 +2,8 @@ cloud.google.com/go v0.30.0 h1:xKvyLgk56d0nksWq49J0UyGEeUIicTl4+UBiX1NPX9g=
cloud.google.com/go v0.30.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
@@ -16,10 +18,16 @@ github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0Pr
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da h1:FjHUJJ7oBW4G/9j1KzlHaXL09LyMVM9rupS39lncbXk=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/lestrrat-go/jwx v0.9.0 h1:Fnd0EWzTm0kFrBPzE/PEPp9nzllES5buMkksPMjEKpM=
github.com/lestrrat-go/jwx v0.9.0/go.mod h1:iEoxlYfZjvoGpuWwxUz+eR5e6KTJGsaRcy/YNA/UnBk=
github.com/lestrrat/jwx v0.9.0 h1:sxyUKCQ0KpX4+GPvSu9lAS0tIwpg7F/O8p/HqyZL4ns=
github.com/lestrrat/jwx v0.9.0/go.mod h1:Ogdl8bCZz7p5/jj4RY2LQTceY/c+AoTIk9gJY+KP4H0=
github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c h1:3wkDRdxK92dF+c1ke2dtj7ZzemFWBHB9plnJOtlwdFA=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+16
View File
@@ -10,6 +10,7 @@ package gothic
import (
"bytes"
"compress/gzip"
"context"
"crypto/rand"
"encoding/base64"
"errors"
@@ -35,6 +36,11 @@ var defaultStore sessions.Store
var keySet = false
type key int
// ProviderParamKey can be used as a key in context when passing in a provider
const ProviderParamKey key = iota
func init() {
key := []byte(os.Getenv("SESSION_SECRET"))
keySet = len(key) != 0
@@ -265,6 +271,11 @@ func getProviderName(req *http.Request) (string, error) {
return p, nil
}
// try to get it from the go-context's value of providerContextKey key
if p, ok := req.Context().Value(ProviderParamKey).(string); ok {
return p, nil
}
// As a fallback, loop over the used providers, if we already have a valid session for any provider (ie. user has already begun authentication with a provider), then return that provider name
providers := goth.GetProviders()
session, _ := Store.Get(req, SessionName)
@@ -280,6 +291,11 @@ func getProviderName(req *http.Request) (string, error) {
return "", errors.New("you must select a provider")
}
// GetContextWithProvider returns a new request context containing the provider
func GetContextWithProvider(req *http.Request, provider string) *http.Request {
return req.WithContext(context.WithValue(req.Context(), ProviderParamKey, provider))
}
// StoreInSession stores a specified key/value pair in the session.
func StoreInSession(key string, value string, req *http.Request, res http.ResponseWriter) error {
session, _ := Store.New(req, SessionName)
+6 -3
View File
@@ -10,7 +10,6 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
@@ -105,7 +104,9 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
}
response, err := p.Client().Get(p.profileURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
req, err := http.NewRequest("GET", p.profileURL, nil)
req.Header.Add("Authorization", "Bearer "+sess.AccessToken)
response, err := p.Client().Do(req)
if err != nil {
return user, err
}
@@ -172,7 +173,9 @@ func userFromReader(reader io.Reader, user *goth.User) error {
}
func getPrivateMail(p *Provider, sess *Session) (email string, err error) {
response, err := p.Client().Get(p.emailURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
req, err := http.NewRequest("GET", p.emailURL, nil)
req.Header.Add("Authorization", "Bearer "+sess.AccessToken)
response, err := p.Client().Do(req)
if err != nil {
if response != nil {
response.Body.Close()
+5 -8
View File
@@ -6,12 +6,13 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)
const (
@@ -173,6 +174,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
RefreshToken: sess.RefreshToken,
ExpiresAt: expiresAt,
RawData: claims,
IDToken: sess.IDToken,
}
p.userFromClaims(claims, &user)
@@ -391,13 +393,8 @@ func decodeJWT(jwt string) (map[string]interface{}, error) {
return nil, errors.New("jws: invalid token received, not all parts available")
}
// Re-pad, if needed
encodedPayload := jwtParts[1]
if l := len(encodedPayload) % 4; l != 0 {
encodedPayload += strings.Repeat("=", 4-l)
}
decodedPayload, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[1])
decodedPayload, err := base64.StdEncoding.DecodeString(encodedPayload)
if err != nil {
return nil, err
}
+1
View File
@@ -27,4 +27,5 @@ type User struct {
AccessTokenSecret string
RefreshToken string
ExpiresAt time.Time
IDToken string
}

Some files were not shown because too many files have changed in this diff Show More