Merge remote-tracking branch 'origin/master' into rebase-merge-commit

This commit is contained in:
Julian Tölle 2018-07-23 18:58:01 +02:00
commit 5bfe7e4467
704 changed files with 20006 additions and 171199 deletions

View File

@ -4,7 +4,7 @@ workspace:
clone: clone:
git: git:
image: plugins/git:1 image: plugins/git:next
depth: 50 depth: 50
tags: true tags: true
@ -75,6 +75,7 @@ pipeline:
- make lint - make lint
- make fmt-check - make fmt-check
- make swagger-check - make swagger-check
# - make swagger-validate
- make misspell-check - make misspell-check
- make test-vendor - make test-vendor
- make build - make build
@ -202,7 +203,7 @@ pipeline:
when: when:
event: [ push, tag ] event: [ push, tag ]
build_docs: build-docs:
image: webhippie/hugo:latest image: webhippie/hugo:latest
pull: true pull: true
commands: commands:
@ -211,26 +212,12 @@ pipeline:
- make clean - make clean
- make build - make build
docker_docs: publish-docs:
image: plugins/docker:17.05 image: lucap/drone-netlify:latest
pull: true pull: true
secrets: [ docker_username, docker_password ] secrets: [ netlify_token ]
repo: gitea/docs site_id: d2260bae-7861-4c02-8646-8f6440b12672
context: docs path: docs/public/
dockerfile: docs/Dockerfile
tags: [ '${DRONE_BRANCH##release/v}' ]
when:
event: [ push ]
branch: [ release/* ]
docker_docs:
image: plugins/docker:17.05
pull: true
secrets: [ docker_username, docker_password ]
repo: gitea/docs
context: docs
dockerfile: docs/Dockerfile
tags: [ 'latest' ]
when: when:
event: [ push ] event: [ push ]
branch: [ master ] branch: [ master ]
@ -254,6 +241,18 @@ pipeline:
when: when:
event: [ push, tag ] event: [ push, tag ]
gpg-sign:
image: plugins/gpgsign:1
pull: true
secrets: [ gpgsign_key, gpgsign_passphrase ]
detach_sign: true
files:
- dist/release/*
excludes:
- dist/release/*.sha256
when:
event: [ push, tag ]
release: release:
image: plugins/s3:1 image: plugins/s3:1
pull: true pull: true

51
BSDmakefile Normal file
View File

@ -0,0 +1,51 @@
# GNU makefile proxy script for BSD make
# Written and maintained by Mahmoud Al-Qudsi <mqudsi@neosmart.net>
# Copyright NeoSmart Technologies <https://neosmart.net/> 2014-2018
# Obtain updates from <https://github.com/neosmart/gmake-proxy>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
JARG =
GMAKE = "gmake"
#When gmake is called from another make instance, -w is automatically added
#which causes extraneous messages about directory changes to be emitted.
#--no-print-directory silences these messages.
GARGS = "--no-print-directory"
.if "$(.MAKE.JOBS)" != ""
JARG = -j$(.MAKE.JOBS)
.endif
#by default bmake will cd into ./obj first
.OBJDIR: ./
.PHONY: FRC
$(.TARGETS): FRC
$(GMAKE) $(GARGS) $(.TARGETS:S,.DONE,,) $(JARG)
.DONE .DEFAULT: .SILENT
$(GMAKE) $(GARGS) $(.TARGETS:S,.DONE,,) $(JARG)
.ERROR: .SILENT
if ! which $(GMAKE) > /dev/null; then \
echo "GNU Make is required!"; \
fi

View File

@ -4,6 +4,81 @@ 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 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). been added to each release, please refer to the [blog](https://blog.gitea.io).
## [1.5.0-RC1](https://github.com/go-gitea/gitea/releases/tag/v1.5.0-rc1) - 2018-07-04
* SECURITY
* Limit uploaded avatar image-size to 4096px x 3072px by default (#4353)
* Do not allow to reuse TOTP passcode (#3878)
* FEATURE
* Add cli commands to regen hooks & keys (#3979)
* Add support for FIDO U2F (#3971)
* Added user language setting (#3875)
* LDAP Public SSH Keys synchronization (#1844)
* Add topic support (#3711)
* Multiple assignees (#3705)
* Add protected branch whitelists for merging (#3689)
* Global code search support (#3664)
* Add label descriptions (#3662)
* Add issue search via API (#3612)
* Add repository setting to enable/disable health checks (#3607)
* Emoji Autocomplete (#3433)
* Implements generator cli for secrets (#3531)
* ENHANCEMENT
* Add more webhooks support and refactor webhook templates directory (#3929)
* Add new option to allow only OAuth2/OpenID user registration (#3910)
* Add option to use paged LDAP search when synchronizing users (#3895)
* Symlink icons (#1416)
* Improve release page UI (#3693)
* Add admin dashboard option to run health checks (#3606)
* Add branch link in branch list (#3576)
* Reduce sql query times in retrieveFeeds (#3547)
* Option to enable or disable swagger endpoints (#3502)
* Add missing licenses (#3497)
* Reduce repo indexer disk usage (#3452)
* Enable caching on assets and avatars (#3376)
* Add repository search ordered by stars/forks. Forks column in admin repo list (#3969)
* Add Environment Variables to Docker template (#4012)
* LFS: make HTTP auth period configurable (#4035)
* Add config path as an optionial flag when changing pass via CLI (#4184)
* Refactor User Settings sections (#3900)
* Allow square brackets in external issue patterns (#3408)
* Add Attachment API (#3478)
* Add EnableTimetracking option to app settings (#3719)
* Add config option to enable or disable log executed SQL (#3726)
* Shows total tracked time in issue and milestone list (#3341)
* TRANSLATION
* Improve English grammar and consistency (#3614)
* DEPLOYMENT
* Allow Gitea to run as different USER in Docker (#3961)
* Provide compressed release binaries (#3991)
* Sign release binaries (#4188)
## [1.4.3](https://github.com/go-gitea/gitea/releases/tag/v1.4.3) - 2018-06-26
* SECURITY
* HTML-escape plain-text READMEs (#4192) (#4214)
* Fix open redirect vulnerability on login screen (#4312) (#4312)
* BUGFIXES
* Fix broken monitoring page when running processes are shown (#4203) (#4208)
* Fix delete comment bug (#4216) (#4228)
* Delete reactions added to issues and comments when deleting repository (#4232) (#4237)
* Fix wiki URL encoding bug (#4091) (#4254)
* Fix code tab link when viewing tags (#3908) (#4263)
* Fix webhook type conflation (#4285) (#4285)
## [1.4.2](https://github.com/go-gitea/gitea/releases/tag/v1.4.2) - 2018-06-04
* BUGFIXES
* Adjust z-index for floating labels (#3939) (#3950)
* Add missing token validation on application settings page (#3976) #3978
* Webhook and hook_task clean up (#4006)
* Fix webhook bug of response info is not displayed in UI (#4023)
* Fix writer cannot read bare repo guide (#4033) (#4039)
* Don't force due date to current time (#3830) (#4057)
* Fix wiki redirects (#3919) (#4065)
* Fix attachment ENABLED (#4064) (#4066)
* Added deletion of an empty line at the end of file (#4054) (#4074)
* Use ResolveReference instead of path.Join (#4073)
* Fix #4081 Check for leading / in base before removing it (#4083)
* Respository's home page not updated after first push (#4075)
## [1.4.1](https://github.com/go-gitea/gitea/releases/tag/v1.4.1) - 2018-05-03 ## [1.4.1](https://github.com/go-gitea/gitea/releases/tag/v1.4.1) - 2018-05-03
* BREAKING * BREAKING
* Add "error" as reserved username (#3882) (#3886) * Add "error" as reserved username (#3882) (#3886)

View File

@ -69,7 +69,7 @@ and keep the compatibility on upgrade. To make sure you are
running the test suite exactly like we do, you should install running the test suite exactly like we do, you should install
the CLI for [Drone CI](https://github.com/drone/drone), as the CLI for [Drone CI](https://github.com/drone/drone), as
we are using the server for continous testing, following [these we are using the server for continous testing, following [these
instructions](http://readme.drone.io/usage/getting-started-cli). After that, instructions](http://docs.drone.io/cli-installation/). After that,
you can simply call `drone exec --local --build-event "pull_request"` within you can simply call `drone exec --local --build-event "pull_request"` within
your working directory and it will try to run the test suite locally. your working directory and it will try to run the test suite locally.
@ -114,7 +114,7 @@ pull request workflow to do that. And, we also use [LGTM](http://lgtm.co)
to ensure every PR is reviewed by at least 2 maintainers. to ensure every PR is reviewed by at least 2 maintainers.
Please try to make your pull request easy to review for us. And, please read Please try to make your pull request easy to review for us. And, please read
the *[How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews)* guide; the *[How to get faster PR reviews](https://github.com/kubernetes/community/blob/261cb0fd089b64002c91e8eddceebf032462ccd6/contributors/guide/pull-requests.md#best-practices-for-faster-reviews)* guide;
it has lots of useful tips for any project you may want to contribute. it has lots of useful tips for any project you may want to contribute.
Some of the key points: Some of the key points:
@ -201,6 +201,10 @@ an advisor has time to code review, we will gladly welcome them back
to the maintainers team. If a maintainer is inactive for more than 3 to the maintainers team. If a maintainer is inactive for more than 3
months and forgets to leave the maintainers team, the owners may move months and forgets to leave the maintainers team, the owners may move
him or her from the maintainers team to the advisors team. him or her from the maintainers team to the advisors team.
For security reasons, Maintainers should use 2FA for their accounts and
if possible provide gpg signed commits.
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
https://help.github.com/articles/signing-commits-with-gpg/
## Owners ## Owners
@ -211,6 +215,9 @@ be the main owner, and the other two the assistant owners. When the new
owners have been elected, the old owners will give up ownership to the owners have been elected, the old owners will give up ownership to the
newly elected owners. If an owner is unable to do so, the other owners newly elected owners. If an owner is unable to do so, the other owners
will assist in ceding ownership to the newly elected owners. will assist in ceding ownership to the newly elected owners.
For security reasons, Owners or any account with write access (like a bot)
must use 2FA.
https://help.github.com/articles/securing-your-account-with-two-factor-authentication-2fa/
After the election, the new owners should proactively agree After the election, the new owners should proactively agree
with our [CONTRIBUTING](CONTRIBUTING.md) requirements in the with our [CONTRIBUTING](CONTRIBUTING.md) requirements in the

View File

@ -58,3 +58,4 @@ CMD ["/bin/s6-svscan", "/etc/s6"]
COPY docker / COPY docker /
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
RUN ln -s /app/gitea/gitea /usr/local/bin/gitea

186
Gopkg.lock generated
View File

@ -11,7 +11,7 @@
branch = "master" branch = "master"
name = "code.gitea.io/sdk" name = "code.gitea.io/sdk"
packages = ["gitea"] packages = ["gitea"]
revision = "b2308e3f700875a3642a78bd3f6e5db8ef6f974d" revision = "ec80752c9512cf07fc62ddc42565118183743942"
[[projects]] [[projects]]
name = "github.com/PuerkitoBio/goquery" name = "github.com/PuerkitoBio/goquery"
@ -143,17 +143,6 @@
packages = ["."] packages = ["."]
revision = "098da33fde5f9220736531b3cb26a2dec86a8367" revision = "098da33fde5f9220736531b3cb26a2dec86a8367"
[[projects]]
name = "github.com/coreos/etcd"
packages = ["error"]
revision = "01c303113d0a3d5a8075864321c3aedb72035bdd"
[[projects]]
branch = "master"
name = "github.com/coreos/go-etcd"
packages = ["etcd"]
revision = "003851be7bb0694fe3cc457a49529a19388ee7cf"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "github.com/couchbase/vellum" name = "github.com/couchbase/vellum"
@ -294,27 +283,24 @@
[[projects]] [[projects]]
name = "github.com/go-sql-driver/mysql" name = "github.com/go-sql-driver/mysql"
packages = ["."] packages = ["."]
revision = "ce924a41eea897745442daaa1739089b0f3f561d" revision = "d523deb1b23d913de5bdada721a6071e71283618"
[[projects]] [[projects]]
name = "github.com/go-xorm/builder" name = "github.com/go-xorm/builder"
packages = ["."] packages = ["."]
revision = "488224409dd8aa2ce7a5baf8d10d55764a913738" revision = "dc8bf48f58fab2b4da338ffd25191905fd741b8f"
version = "v0.3.0"
[[projects]] [[projects]]
name = "github.com/go-xorm/core" name = "github.com/go-xorm/core"
packages = ["."] packages = ["."]
revision = "cb1d0ca71f42d3ee1bf4aba7daa16099bc31a7e9" revision = "c10e21e7e1cec20e09398f2dfae385e58c8df555"
version = "v0.6.0"
[[projects]]
name = "github.com/go-xorm/tidb"
packages = ["."]
revision = "21e49190ce47a766fa741cf7edc831a30c12c6ac"
[[projects]] [[projects]]
name = "github.com/go-xorm/xorm" name = "github.com/go-xorm/xorm"
packages = ["."] packages = ["."]
revision = "d4149d1eee0c2c488a74a5863fd9caf13d60fd03" revision = "ad69f7d8f0861a29438154bb0a20b60501298480"
[[projects]] [[projects]]
branch = "master" branch = "master"
@ -370,11 +356,6 @@
packages = ["."] packages = ["."]
revision = "8fb95d837f7d6db1913fecfd7bcc5333e6499596" revision = "8fb95d837f7d6db1913fecfd7bcc5333e6499596"
[[projects]]
name = "github.com/juju/errors"
packages = ["."]
revision = "b2c7a7da5b2995941048f60146e67702a292e468"
[[projects]] [[projects]]
name = "github.com/kballard/go-shellquote" name = "github.com/kballard/go-shellquote"
packages = ["."] packages = ["."]
@ -497,134 +478,12 @@
packages = ["."] packages = ["."]
revision = "891127d8d1b52734debe1b3c3d7e747502b6c366" revision = "891127d8d1b52734debe1b3c3d7e747502b6c366"
[[projects]]
name = "github.com/ngaut/deadline"
packages = ["."]
revision = "fae8f9dfd7048de16575b9d4c255278e38c28a4f"
[[projects]]
branch = "master"
name = "github.com/ngaut/go-zookeeper"
packages = ["zk"]
revision = "9c3719e318c7cfd072e41eb48cb71fcaa49d5e05"
[[projects]]
name = "github.com/ngaut/log"
packages = ["."]
revision = "d2af3a61f64d093457fb23b25d20f4ce3cd551ce"
[[projects]]
branch = "master"
name = "github.com/ngaut/pools"
packages = ["."]
revision = "b7bc8c42aac787667ba45adea78233f53f548443"
[[projects]]
branch = "master"
name = "github.com/ngaut/sync2"
packages = ["."]
revision = "7a24ed77b2efb460c1468b7dc917821c66e80e55"
[[projects]]
branch = "master"
name = "github.com/ngaut/tso"
packages = [
"client",
"proto",
"util"
]
revision = "118f6c141d58f1e72577ff61f43f649bf39355ee"
[[projects]]
branch = "master"
name = "github.com/ngaut/zkhelper"
packages = ["."]
revision = "6738bdc138d469112c6687fbfcfe049ccabd6a0a"
[[projects]]
branch = "master"
name = "github.com/petar/GoLLRB"
packages = ["llrb"]
revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4"
[[projects]] [[projects]]
name = "github.com/philhofer/fwd" name = "github.com/philhofer/fwd"
packages = ["."] packages = ["."]
revision = "bb6d471dc95d4fe11e432687f8b70ff496cf3136" revision = "bb6d471dc95d4fe11e432687f8b70ff496cf3136"
version = "v1.0.0" version = "v1.0.0"
[[projects]]
name = "github.com/pingcap/go-hbase"
packages = [
".",
"iohelper",
"proto"
]
revision = "7a98d1fe4e9e115de8c77ae0e158c0d08732c550"
[[projects]]
branch = "master"
name = "github.com/pingcap/go-themis"
packages = [
".",
"oracle",
"oracle/oracles"
]
revision = "dbb996606c1d1fe8571fd9ac6da2254c76d2c5c9"
[[projects]]
name = "github.com/pingcap/tidb"
packages = [
".",
"ast",
"column",
"context",
"ddl",
"domain",
"evaluator",
"executor",
"infoschema",
"inspectkv",
"kv",
"kv/memkv",
"meta",
"meta/autoid",
"model",
"mysql",
"optimizer",
"optimizer/plan",
"parser",
"parser/opcode",
"perfschema",
"privilege",
"privilege/privileges",
"sessionctx",
"sessionctx/autocommit",
"sessionctx/db",
"sessionctx/forupdate",
"sessionctx/variable",
"store/hbase",
"store/localstore",
"store/localstore/boltdb",
"store/localstore/engine",
"store/localstore/goleveldb",
"structure",
"table",
"table/tables",
"terror",
"util",
"util/bytes",
"util/charset",
"util/codec",
"util/distinct",
"util/hack",
"util/segmentmap",
"util/sqlexec",
"util/stringutil",
"util/types"
]
revision = "33197485abe227dcb254644cf5081c9a3c281669"
[[projects]] [[projects]]
name = "github.com/pmezard/go-difflib" name = "github.com/pmezard/go-difflib"
packages = ["difflib"] packages = ["difflib"]
@ -673,24 +532,6 @@
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1" version = "v1.2.1"
[[projects]]
name = "github.com/syndtr/goleveldb"
packages = [
"leveldb",
"leveldb/cache",
"leveldb/comparer",
"leveldb/errors",
"leveldb/filter",
"leveldb/iterator",
"leveldb/journal",
"leveldb/memdb",
"leveldb/opt",
"leveldb/storage",
"leveldb/table",
"leveldb/util"
]
revision = "917f41c560270110ceb73c5b38be2a9127387071"
[[projects]] [[projects]]
branch = "master" branch = "master"
name = "github.com/tinylib/msgp" name = "github.com/tinylib/msgp"
@ -703,17 +544,6 @@
packages = ["."] packages = ["."]
revision = "d21a03e0b1d9fc1df59ff54e7a513655c1748b0c" revision = "d21a03e0b1d9fc1df59ff54e7a513655c1748b0c"
[[projects]]
name = "github.com/twinj/uuid"
packages = ["."]
revision = "89173bcdda19db0eb88aef1e1cb1cb2505561d31"
version = "0.10.0"
[[projects]]
name = "github.com/ugorji/go"
packages = ["codec"]
revision = "c062049c1793b01a3cc3fe786108edabbaf7756b"
[[projects]] [[projects]]
name = "github.com/urfave/cli" name = "github.com/urfave/cli"
packages = ["."] packages = ["."]
@ -873,6 +703,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "036b8c882671cf8d2c5e2fdbe53b1bdfbd39f7ebd7765bd50276c7c4ecf16687" inputs-digest = "5ae18d543bbb8186589c003422b333097d67bb5fed8b4c294be70c012ccffc94"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@ -30,15 +30,14 @@ ignored = ["google.golang.org/appengine*"]
revision = "f2499483f923065a842d38eb4c7f1927e6fc6e6d" revision = "f2499483f923065a842d38eb4c7f1927e6fc6e6d"
name = "golang.org/x/net" name = "golang.org/x/net"
[[constraint]]
#version = "v1.0.0"
revision = "33197485abe227dcb254644cf5081c9a3c281669"
name = "github.com/pingcap/tidb"
[[override]] [[override]]
name = "github.com/go-xorm/xorm" name = "github.com/go-xorm/xorm"
#version = "0.6.5" #version = "0.6.5"
revision = "d4149d1eee0c2c488a74a5863fd9caf13d60fd03" revision = "ad69f7d8f0861a29438154bb0a20b60501298480"
[[override]]
name = "github.com/go-sql-driver/mysql"
revision = "d523deb1b23d913de5bdada721a6071e71283618"
[[override]] [[override]]
name = "github.com/gorilla/mux" name = "github.com/gorilla/mux"

View File

@ -22,3 +22,5 @@ Peter Žeby <morlinest@gmail.com> (@morlinest)
Matti Ranta <matti@mdranta.net> (@techknowlogick) Matti Ranta <matti@mdranta.net> (@techknowlogick)
Michael Lustfield <mtecknology@debian.org> (@MTecknology) Michael Lustfield <mtecknology@debian.org> (@MTecknology)
Jonas Franz <info@jonasfranz.software> (@JonasFranzDEV) Jonas Franz <info@jonasfranz.software> (@JonasFranzDEV)
Flynn Lufmons <fluf@warpmail.net> (@flufmonster)
Alexey Terentyev <axifnx@gmail.com> (@axifive)

View File

@ -21,7 +21,19 @@ GOFMT ?= gofmt -s
GOFLAGS := -i -v GOFLAGS := -i -v
EXTRA_GOFLAGS ?= EXTRA_GOFLAGS ?=
LDFLAGS := -X "main.Version=$(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')" -X "main.Tags=$(TAGS)" ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))
GITEA_VERSION := $(VERSION)
else
ifneq ($(DRONE_BRANCH),)
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
else
VERSION ?= master
endif
GITEA_VERSION := $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
endif
LDFLAGS := -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
PACKAGES ?= $(filter-out code.gitea.io/gitea/integrations,$(shell $(GO) list ./... | grep -v /vendor/)) PACKAGES ?= $(filter-out code.gitea.io/gitea/integrations,$(shell $(GO) list ./... | grep -v /vendor/))
SOURCES ?= $(shell find . -name "*.go" -type f) SOURCES ?= $(shell find . -name "*.go" -type f)
@ -45,15 +57,8 @@ else
EXECUTABLE := gitea EXECUTABLE := gitea
endif endif
ifneq ($(DRONE_TAG),) # $(call strip-suffix,filename)
VERSION ?= $(subst v,,$(DRONE_TAG)) strip-suffix = $(firstword $(subst ., ,$(1)))
else
ifneq ($(DRONE_BRANCH),)
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))
else
VERSION ?= master
endif
endif
.PHONY: all .PHONY: all
all: build all: build
@ -100,6 +105,13 @@ swagger-check: generate-swagger
exit 1; \ exit 1; \
fi; fi;
.PHONY: swagger-validate
swagger-validate:
@hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/go-swagger/go-swagger/cmd/swagger; \
fi
swagger validate ./public/swagger.v1.json
.PHONY: errcheck .PHONY: errcheck
errcheck: errcheck:
@hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash errcheck > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
@ -294,7 +306,7 @@ public/js/index.js: $(JAVASCRIPTS)
.PHONY: stylesheets-check .PHONY: stylesheets-check
stylesheets-check: generate-stylesheets stylesheets-check: generate-stylesheets
@diff=$$(git diff public/css/index.css); \ @diff=$$(git diff public/css/*); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make generate-stylesheets' and commit the result:"; \ echo "Please run 'make generate-stylesheets' and commit the result:"; \
echo "$${diff}"; \ echo "$${diff}"; \
@ -304,6 +316,7 @@ stylesheets-check: generate-stylesheets
.PHONY: generate-stylesheets .PHONY: generate-stylesheets
generate-stylesheets: generate-stylesheets:
node_modules/.bin/lessc --clean-css public/less/index.less public/css/index.css node_modules/.bin/lessc --clean-css public/less/index.less public/css/index.css
$(foreach file, $(filter-out public/less/themes/_base.less, $(wildcard public/less/themes/*)),node_modules/.bin/lessc --clean-css public/less/themes/$(notdir $(file)) > public/css/theme-$(notdir $(call strip-suffix,$(file))).css;)
.PHONY: swagger-ui .PHONY: swagger-ui
swagger-ui: swagger-ui:

View File

@ -12,13 +12,6 @@
[![Help Contribute to Open Source](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea) [![Help Contribute to Open Source](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea)
[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backer/badge.svg?label=backer&color=brightgreen)](https://opencollective.com/gitea) [![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/backer/badge.svg?label=backer&color=brightgreen)](https://opencollective.com/gitea)
| | | |
|:---:|:---:|:---:|
|![Dashboard](https://image.ibb.co/dms6DG/1.png)|![Repository](https://image.ibb.co/m6MSLw/2.png)|![Commits History](https://image.ibb.co/cjrSLw/3.png)|
|![Branches](https://image.ibb.co/e6vbDG/4.png)|![Issues](https://image.ibb.co/bJTJSb/5.png)|![Pull Request View](https://image.ibb.co/e02dSb/6.png)|
|![Releases](https://image.ibb.co/cUzgfw/7.png)|![Activity](https://image.ibb.co/eZgGDG/8.png)|![Wiki](https://image.ibb.co/dYV9YG/9.png)|
|![Diff](https://image.ibb.co/ewA9YG/10.png)|![Organization](https://image.ibb.co/ceOwDG/11.png)|![Profile](https://image.ibb.co/c44Q7b/12.png)|
## Purpose ## Purpose
The goal of this project is to make the easiest, fastest, and most The goal of this project is to make the easiest, fastest, and most
@ -91,8 +84,28 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a> <a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a> <a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
## FAQ
**How do you pronounce Gitea?**
Gitea is pronounced [/ɡɪti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g.
**Why is this not hosted on a Gitea instance?**
We're [working on it](https://github.com/go-gitea/gitea/issues/1029).
## License ## License
This project is licensed under the MIT License. This project is licensed under the MIT License.
See the [LICENSE](https://github.com/go-gitea/gitea/blob/master/LICENSE) file See the [LICENSE](https://github.com/go-gitea/gitea/blob/master/LICENSE) file
for the full license text. for the full license text.
## Screenshots
Looking for an overview of the interface? Check it out!
| | | |
|:---:|:---:|:---:|
|![Dashboard](https://image.ibb.co/dms6DG/1.png)|![Repository](https://image.ibb.co/m6MSLw/2.png)|![Commits History](https://image.ibb.co/cjrSLw/3.png)|
|![Branches](https://image.ibb.co/e6vbDG/4.png)|![Issues](https://image.ibb.co/bJTJSb/5.png)|![Pull Request View](https://image.ibb.co/e02dSb/6.png)|
|![Releases](https://image.ibb.co/cUzgfw/7.png)|![Activity](https://image.ibb.co/eZgGDG/8.png)|![Wiki](https://image.ibb.co/dYV9YG/9.png)|
|![Diff](https://image.ibb.co/ewA9YG/10.png)|![Organization](https://image.ibb.co/ceOwDG/11.png)|![Profile](https://image.ibb.co/c44Q7b/12.png)|

View File

@ -73,6 +73,11 @@ var (
Value: "", Value: "",
Usage: "New password to set for user", Usage: "New password to set for user",
}, },
cli.StringFlag{
Name: "config, c",
Value: "custom/conf/app.ini",
Usage: "Custom configuration file path",
},
}, },
} }
@ -123,6 +128,10 @@ func runChangePassword(c *cli.Context) error {
return err return err
} }
if c.IsSet("config") {
setting.CustomConf = c.String("config")
}
if err := initDB(); err != nil { if err := initDB(); err != nil {
return err return err
} }

View File

@ -268,7 +268,7 @@ func runServ(c *cli.Context) error {
claims := jwt.MapClaims{ claims := jwt.MapClaims{
"repo": repo.ID, "repo": repo.ID,
"op": lfsVerb, "op": lfsVerb,
"exp": now.Add(5 * time.Minute).Unix(), "exp": now.Add(setting.LFS.HTTPAuthExpiry).Unix(),
"nbf": now.Unix(), "nbf": now.Unix(),
} }
if user != nil { if user != nil {

View File

@ -24,8 +24,8 @@
# Default values # Default values
NAME=gitea NAME=gitea
GITEA_HOME=/home/git/gitea GITEA_HOME=/var/lib/${NAME}
GITEA_PATH=${GITEA_HOME}/$NAME GITEA_PATH=/usr/local/bin/${NAME}
GITEA_USER=git GITEA_USER=git
SERVICENAME="Gitea - Git with a cup of tea" SERVICENAME="Gitea - Git with a cup of tea"
LOCKFILE=/var/lock/subsys/gitea LOCKFILE=/var/lock/subsys/gitea
@ -49,11 +49,11 @@ DAEMON_OPTS="--check $NAME"
start() { start() {
cd ${GITEA_HOME} cd ${GITEA_HOME}
echo -n "Starting ${SERVICENAME}: " echo -n "Starting ${SERVICENAME}: "
daemon $DAEMON_OPTS "${GITEA_PATH} web > ${LOGFILE} 2>&1 &" daemon $DAEMON_OPTS "${GITEA_PATH} web -c /etc/${NAME}/app.ini > ${LOGFILE} 2>&1 &"
RETVAL=$? RETVAL=$?
echo echo
[ $RETVAL = 0 ] && touch ${LOCKFILE} [ $RETVAL = 0 ] && touch ${LOCKFILE}
return $RETVAL return $RETVAL
} }
@ -63,7 +63,7 @@ stop() {
killproc ${NAME} killproc ${NAME}
RETVAL=$? RETVAL=$?
echo echo
[ $RETVAL = 0 ] && rm -f ${LOCKFILE} [ $RETVAL = 0 ] && rm -f ${LOCKFILE}
} }
case "$1" in case "$1" in

View File

@ -14,17 +14,20 @@
# Do NOT "set -e" # Do NOT "set -e"
# PATH should only include /usr/* if it runs after the mountnfs.sh script # PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
DESC="Git with a cup of tea" DESC="Gitea - Git with a cup of tea"
NAME=gitea NAME=gitea
SERVICEVERBOSE=yes SERVICEVERBOSE=yes
PIDFILE=/var/run/$NAME.pid PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME SCRIPTNAME=/etc/init.d/$NAME
WORKINGDIR=/home/git/gitea WORKINGDIR=/var/lib/$NAME
DAEMON=$WORKINGDIR/$NAME DAEMON=/usr/local/bin/$NAME
DAEMON_ARGS="web" DAEMON_ARGS="web -c /etc/$NAME/app.ini"
USER=git USER=git
USERBIND="setcap cap_net_bind_service=+ep" USERBIND=""
# If you want to bind Gitea to a port below 1024 uncomment
# the line below
#USERBIND="setcap cap_net_bind_service=+ep"
STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/1/KILL/5}" STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/1/KILL/5}"
# Read configuration variable file if it is present # Read configuration variable file if it is present
@ -36,7 +39,7 @@ STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/1/KILL/5}"
do_start() do_start()
{ {
$USERBIND $DAEMON $USERBIND $DAEMON
sh -c "USER=$USER start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \\ sh -c "USER=$USER HOME=/home/$USER GITEA_WORK_DIR=$WORKINGDIR start-stop-daemon --start --quiet --pidfile $PIDFILE --make-pidfile \\
--background --chdir $WORKINGDIR --chuid $USER \\ --background --chdir $WORKINGDIR --chuid $USER \\
--exec $DAEMON -- $DAEMON_ARGS" --exec $DAEMON -- $DAEMON_ARGS"
} }

View File

@ -19,9 +19,9 @@ load_rc_config $name
: ${gitea_user:="git"} : ${gitea_user:="git"}
: ${gitea_enable:="NO"} : ${gitea_enable:="NO"}
: ${gitea_directory:="/home/git"} : ${gitea_directory:="/var/lib/gitea"}
command="${gitea_directory}/gitea web" command="/usr/local/bin/gitea web -c /etc/gitea/app.ini"
procname="$(echo $command |cut -d' ' -f1)" procname="$(echo $command |cut -d' ' -f1)"
pidfile="${gitea_directory}/${name}.pid" pidfile="${gitea_directory}/${name}.pid"
@ -33,6 +33,7 @@ gitea_start() {
cd ${gitea_directory} cd ${gitea_directory}
export USER=${gitea_user} export USER=${gitea_user}
export HOME=/usr/home/${gitea_user} export HOME=/usr/home/${gitea_user}
export GITEA_WORK_DIR=${gitea_directory}
/usr/sbin/daemon -f -u ${gitea_user} -p ${pidfile} $command /usr/sbin/daemon -f -u ${gitea_user} -p ${pidfile} $command
} }

View File

@ -1,11 +1,11 @@
#!/sbin/openrc-run #!/sbin/openrc-run
DIR=/home/git/gitea DIR=/var/lib/gitea
USER=git USER=git
start_stop_daemon_args="--user ${USER} --chdir ${DIR}" start_stop_daemon_args="--user ${USER} --chdir ${DIR}"
command="${DIR}/gitea" command="/usr/local/bin/gitea"
command_args="web" command_args="web -c /etc/gitea/app.ini"
command_background=yes command_background=yes
pidfile=/var/run/gitea.pid pidfile=/var/run/gitea.pid

View File

@ -2,11 +2,11 @@
# #
# $OpenBSD$ # $OpenBSD$
daemon="/home/git/gitea/gitea" daemon="/usr/local/bin/gitea"
daemon_user="git" daemon_user="git"
daemon_flags="web" daemon_flags="web -c /etc/gitea/app.ini"
gitea_directory="/home/git/gitea" gitea_directory="/var/lib/gitea"
rc_bg=YES rc_bg=YES

View File

@ -0,0 +1,46 @@
<?xml version="1.0"?>
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<service_bundle type="manifest" name="export">
<service name="gitea" type="service" version="1">
<create_default_instance enabled="false"/>
<dependency name="network" grouping="require_all" restart_on="refresh" type="service">
<service_fmri value="svc:/milestone/network:default"/>
</dependency>
<dependency name="filesystem" grouping="require_all" restart_on="refresh" type="service">
<service_fmri value="svc:/system/filesystem/local"/>
</dependency>
<exec_method
type="method"
name="start"
exec="/opt/local/bin/gitea web"
timeout_seconds="60">
<method_context>
<method_credential user="git" group="git" />
<method_environment>
<envvar name='GITEA_WORK_DIR' value='/opt/local/share/gitea'/>
<envvar name='GITEA_CUSTOM' value='/opt/local/etc/gitea'/>
<envvar name='HOME' value='/var/db/gitea'/>
<envvar name='PATH' value='/opt/local/bin:${PATH}'/>
<envvar name='USER' value='git'/>
</method_environment>
</method_context>
</exec_method>
<exec_method type="method" name="stop" exec=":kill" timeout_seconds="60"/>
<property_group name="application" type="application"></property_group>
<property_group name="startd" type="framework">
<propval name="duration" type="astring" value="child"/>
<propval name="ignore_error" type="astring" value="core,signal"/>
</property_group>
<template>
<common_name>
<loctext xml:lang="C">A painless, self-hosted Git service</loctext>
</common_name>
</template>
</service>
</service_bundle>

View File

@ -18,10 +18,10 @@
# Default values # Default values
NAME=gitea NAME=gitea
GITEA_HOME=/home/git/gitea GITEA_HOME=/var/lib/$NAME
GITEA_PATH=${GITEA_HOME}/$NAME GITEA_PATH=/usr/local/bin/$NAME
GITEA_USER=git GITEA_USER=git
SERVICENAME="Git - with a cup of tea" SERVICENAME="Gitea - Git with a cup of tea"
LOCKFILE=/var/lock/subsys/gitea LOCKFILE=/var/lock/subsys/gitea
LOGPATH=${GITEA_HOME}/log LOGPATH=${GITEA_HOME}/log
LOGFILE=${LOGPATH}/error.log LOGFILE=${LOGPATH}/error.log
@ -58,7 +58,7 @@ case "$1" in
# return skipped as service is already running # return skipped as service is already running
(exit 5) (exit 5)
else else
su - ${GITEA_USER} -c "USER=${GITEA_USER} ${GITEA_PATH} web 2>&1 >>${LOGFILE} &" su - ${GITEA_USER} -c "USER=${GITEA_USER} GITEA_WORK_DIR=${GITEA_HOME} ${GITEA_PATH} web -c /etc/${NAME}/app.ini 2>&1 >>${LOGFILE} &"
fi fi
# Remember status and be verbose # Remember status and be verbose

View File

@ -18,10 +18,10 @@ RestartSec=2s
Type=simple Type=simple
User=git User=git
Group=git Group=git
WorkingDirectory=/home/git/gitea WorkingDirectory=/var/lib/gitea/
ExecStart=/home/git/gitea/gitea web ExecStart=/usr/local/bin/gitea web -c /etc/gitea/app.ini
Restart=always Restart=always
Environment=USER=git HOME=/home/git Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
# If you want to bind Gitea to a port below 1024 uncomment # If you want to bind Gitea to a port below 1024 uncomment
# the two values below # the two values below
### ###

View File

@ -67,6 +67,8 @@ EXPLORE_PAGING_NUM = 20
ISSUE_PAGING_NUM = 10 ISSUE_PAGING_NUM = 10
; Number of maximum commits displayed in one activity feed ; Number of maximum commits displayed in one activity feed
FEED_MAX_COMMIT_NUM = 5 FEED_MAX_COMMIT_NUM = 5
; Number of maximum commits displayed in commit graph.
GRAPH_MAX_COMMIT_NUM = 100
; Value of `theme-color` meta tag, used by Android >= 5.0 ; Value of `theme-color` meta tag, used by Android >= 5.0
; An invalid color like "none" or "disable" will have the default style ; An invalid color like "none" or "disable" will have the default style
; More info: https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android ; More info: https://developers.google.com/web/updates/2014/11/Support-for-theme-color-in-Chrome-39-for-Android
@ -75,6 +77,8 @@ THEME_COLOR_META_TAG = `#6cc644`
MAX_DISPLAY_FILE_SIZE = 8388608 MAX_DISPLAY_FILE_SIZE = 8388608
; Whether the email of the user should be shown in the Explore Users page ; Whether the email of the user should be shown in the Explore Users page
SHOW_USER_EMAIL = true SHOW_USER_EMAIL = true
; Set the default theme for the Gitea install
DEFAULT_THEME = gitea
[ui.admin] [ui.admin]
; Number of users that are displayed on one page ; Number of users that are displayed on one page
@ -189,6 +193,8 @@ LFS_START_SERVER = false
LFS_CONTENT_PATH = data/lfs LFS_CONTENT_PATH = data/lfs
; LFS authentication secret, change this yourself ; LFS authentication secret, change this yourself
LFS_JWT_SECRET = LFS_JWT_SECRET =
; LFS authentication validity period (in time.Duration), pushes taking longer than this may fail.
LFS_HTTP_AUTH_EXPIRY = 20m
; Define allowed algorithms and their minimum key length (use -1 to disable a type) ; Define allowed algorithms and their minimum key length (use -1 to disable a type)
[ssh.minimum_key_sizes] [ssh.minimum_key_sizes]
@ -299,13 +305,22 @@ ENABLE_NOTIFY_MAIL = false
ENABLE_REVERSE_PROXY_AUTHENTICATION = false ENABLE_REVERSE_PROXY_AUTHENTICATION = false
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false
; Enable captcha validation for registration ; Enable captcha validation for registration
ENABLE_CAPTCHA = true ENABLE_CAPTCHA = false
; Type of captcha you want to use. Options: image, recaptcha
CAPTCHA_TYPE = image
; Enable recaptcha to use Google's recaptcha service
; Go to https://www.google.com/recaptcha/admin to sign up for a key
RECAPTCHA_SECRET =
RECAPTCHA_SITEKEY =
; Default value for KeepEmailPrivate ; Default value for KeepEmailPrivate
; Each new user will get the value of this setting copied into their profile ; Each new user will get the value of this setting copied into their profile
DEFAULT_KEEP_EMAIL_PRIVATE = false DEFAULT_KEEP_EMAIL_PRIVATE = false
; Default value for AllowCreateOrganization ; Default value for AllowCreateOrganization
; Every new user will have rights set to create organizations depending on this setting ; Every new user will have rights set to create organizations depending on this setting
DEFAULT_ALLOW_CREATE_ORGANIZATION = true DEFAULT_ALLOW_CREATE_ORGANIZATION = true
; Default value for EnableDependencies
; Repositories will use depencies by default depending on this setting
DEFAULT_ENABLE_DEPENDENCIES = true
; Enable Timetracking ; Enable Timetracking
ENABLE_TIMETRACKING = true ENABLE_TIMETRACKING = true
; Default value for EnableTimetracking ; Default value for EnableTimetracking
@ -400,6 +415,10 @@ SESSION_LIFE_TIME = 86400
[picture] [picture]
AVATAR_UPLOAD_PATH = data/avatars AVATAR_UPLOAD_PATH = data/avatars
; Max Width and Height of uploaded avatars. This is to limit the amount of RAM
; used when resizing the image.
AVATAR_MAX_WIDTH = 4096
AVATAR_MAX_HEIGHT = 3072
; Chinese users can choose "duoshuo" ; Chinese users can choose "duoshuo"
; or a custom avatar source, like: http://cn.gravatar.com/avatar/ ; or a custom avatar source, like: http://cn.gravatar.com/avatar/
GRAVATAR_SOURCE = gravatar GRAVATAR_SOURCE = gravatar
@ -412,7 +431,7 @@ ENABLE_FEDERATED_AVATAR = false
[attachment] [attachment]
; Whether attachments are enabled. Defaults to `true` ; Whether attachments are enabled. Defaults to `true`
ENABLE = true ENABLED = true
; Path for attachments. Defaults to `data/attachments` ; Path for attachments. Defaults to `data/attachments`
PATH = data/attachments PATH = data/attachments
; One or more allowed types, e.g. image/jpeg|image/png ; One or more allowed types, e.g. image/jpeg|image/png
@ -599,9 +618,9 @@ ko-KR = ko
[U2F] [U2F]
; Two Factor authentication with security keys ; Two Factor authentication with security keys
; https://developers.yubico.com/U2F/App_ID.html ; https://developers.yubico.com/U2F/App_ID.html
APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
; Comma seperated list of truisted facets ; Comma seperated list of truisted facets
TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/
; Extension mapping to highlight class ; Extension mapping to highlight class
; e.g. .toml=ini ; e.g. .toml=ini

View File

@ -1,7 +1,5 @@
#!/bin/bash #!/bin/bash
/usr/sbin/update-ca-certificates
if [ ! -d /data/git/.ssh ]; then if [ ! -d /data/git/.ssh ]; then
mkdir -p /data/git/.ssh mkdir -p /data/git/.ssh
chmod 700 /data/git/.ssh chmod 700 /data/git/.ssh

View File

@ -4,6 +4,9 @@ RUN_MODE = $RUN_MODE
[repository] [repository]
ROOT = /data/git/repositories ROOT = /data/git/repositories
[repository.local]
LOCAL_COPY_PATH = /data/gitea/tmp/local-repo
[repository.upload] [repository.upload]
TEMP_PATH = /data/gitea/uploads TEMP_PATH = /data/gitea/uploads
@ -14,6 +17,7 @@ HTTP_PORT = $HTTP_PORT
ROOT_URL = $ROOT_URL ROOT_URL = $ROOT_URL
DISABLE_SSH = $DISABLE_SSH DISABLE_SSH = $DISABLE_SSH
SSH_PORT = $SSH_PORT SSH_PORT = $SSH_PORT
LFS_CONTENT_PATH = /data/git/lfs
[database] [database]
PATH = /data/gitea/gitea.db PATH = /data/gitea/gitea.db
@ -23,6 +27,9 @@ NAME = $DB_NAME
USER = $DB_USER USER = $DB_USER
PASSWD = $DB_PASSWD PASSWD = $DB_PASSWD
[indexer]
ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve
[session] [session]
PROVIDER_CONFIG = /data/gitea/sessions PROVIDER_CONFIG = /data/gitea/sessions

View File

@ -1,22 +0,0 @@
# build stage
FROM golang:alpine AS build-env
RUN apk add --no-cache git
RUN go get -d -v github.com/mholt/caddy/caddy github.com/pedronasser/caddy-search github.com/simia-tech/caddy-locale
WORKDIR /go/src/github.com/mholt/caddy/caddy
RUN sed -i '/This is where other plugins get plugged in (imported)/a _ "github.com/pedronasser/caddy-search"' caddymain/run.go \
&& sed -i '/This is where other plugins get plugged in (imported)/a _ "github.com/simia-tech/caddy-locale"' caddymain/run.go \
&& go install -v . \
&& /go/bin/caddy -version
FROM alpine:edge
EXPOSE 80
RUN apk add --no-cache wget mailcap ca-certificates
COPY --from=build-env /go/bin/caddy /usr/sbin/caddy
COPY docker/caddy.conf /etc/caddy.conf
COPY public /srv/www
CMD ["/usr/sbin/caddy", "-conf", "/etc/caddy.conf"]

View File

@ -31,7 +31,7 @@ menu:
post: active post: active
- name: API - name: API
url: https://try.gitea.io/api/swagger url: https://try.gitea.io/api/swagger
weight: 25 weight: 45
pre: plug pre: plug
- name: Blog - name: Blog
url: https://blog.gitea.io/ url: https://blog.gitea.io/
@ -79,7 +79,7 @@ languages:
post: active post: active
- name: API - name: API
url: https://try.gitea.io/api/swagger url: https://try.gitea.io/api/swagger
weight: 25 weight: 45
pre: plug pre: plug
- name: 博客 - name: 博客
url: https://blog.gitea.io/ url: https://blog.gitea.io/
@ -122,7 +122,7 @@ languages:
post: active post: active
- name: API - name: API
url: https://try.gitea.io/api/swagger url: https://try.gitea.io/api/swagger
weight: 25 weight: 45
pre: plug pre: plug
- name: 部落格 - name: 部落格
url: https://blog.gitea.io/ url: https://blog.gitea.io/
@ -165,7 +165,7 @@ languages:
post: active post: active
- name: API - name: API
url: https://try.gitea.io/api/swagger url: https://try.gitea.io/api/swagger
weight: 25 weight: 45
pre: plug pre: plug
- name: Blog - name: Blog
url: https://blog.gitea.io/ url: https://blog.gitea.io/
@ -208,7 +208,7 @@ languages:
post: active post: active
- name: API - name: API
url: https://try.gitea.io/api/swagger url: https://try.gitea.io/api/swagger
weight: 25 weight: 45
pre: plug pre: plug
- name: Blog - name: Blog
url: https://blog.gitea.io/ url: https://blog.gitea.io/
@ -241,17 +241,17 @@ languages:
menu: menu:
page: page:
- name: Site - name: Site
url: /fr-fr/ url: https://gitea.io/en-us/
weight: 10 weight: 10
pre: home pre: home
post: active post: active
- name: Documentation - name: Documentation
url: https://docs.gitea.io/fr-fr/ url: /fr-fr/
weight: 20 weight: 20
pre: question pre: question
- name: API - name: API
url: https://try.gitea.io/api/swagger url: https://try.gitea.io/api/swagger
weight: 25 weight: 45
pre: plug pre: plug
- name: Blog - name: Blog
url: https://blog.gitea.io/ url: https://blog.gitea.io/

View File

@ -0,0 +1,75 @@
---
date: "2018-06-24:00:00+02:00"
title: "API Usage"
slug: "api-usage"
weight: 40
toc: true
draft: false
menu:
sidebar:
parent: "advanced"
name: "API Usage"
weight: 40
identifier: "api-usage"
---
# Gitea API Usage
## Enabling/configuring API access
By default, `ENABLE_SWAGGER_ENDPOINT` is true, and
`MAX_RESPONSE_ITEMS` is set to 50. See [Config Cheat
Sheet](https://docs.gitea.io/en-us/config-cheat-sheet/) for more
information.
## Authentication via the API
Gitea supports these methods of API authentication:
- HTTP basic authentication
- `token=...` parameter in URL query string
- `access_token=...` parameter in URL query string
- `Authorization: token ...` header in HTTP headers
All of these methods accept the same apiKey token type. You can
better understand this by looking at the code -- as of this writing,
Gitea parses queries and headers to find the token in
[modules/auth/auth.go](https://github.com/go-gitea/gitea/blob/6efdcaed86565c91a3dc77631372a9cc45a58e89/modules/auth/auth.go#L47).
You can create an apiKey token via your gitea install's web interface:
`Settings | Applications | Generate New Token`.
### More on the `Authorization:` header
For historical reasons, Gitea needs the word `token` included before
the apiKey token in an authorization header, like this:
```
Authorization: token 65eaa9c8ef52460d22a93307fe0aee76289dc675
```
In a `curl` command, for instance, this would look like:
```
curl -X POST "http://localhost:4000/api/v1/repos/test1/test1/issues" \
-H "accept: application/json" \
-H "Authorization: token 65eaa9c8ef52460d22a93307fe0aee76289dc675" \
-H "Content-Type: application/json" -d "{ \"body\": \"testing\", \"title\": \"test 20\"}" -i
```
As mentioned above, the token used is the same one you would use in
the `token=` string in a GET request.
## Listing your issued tokens via the API
As mentioned in
[#3842](https://github.com/go-gitea/gitea/issues/3842#issuecomment-397743346),
`/users/:name/tokens` is special and requires you to authenticate
using BasicAuth, as follows:
### Using basic authentication:
```
$ curl --request GET --url https://yourusername:yourpassword@gitea.your.host/api/v1/users/yourusername/tokens
[{"name":"test","sha1":"..."},{"name":"dev","sha1":"..."}]
```

View File

@ -68,6 +68,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `EXPLORE_PAGING_NUM`: **20**: Number of repositories that are shown in one explore page. - `EXPLORE_PAGING_NUM`: **20**: Number of repositories that are shown in one explore page.
- `ISSUE_PAGING_NUM`: **10**: Number of issues that are shown in one page (for all pages that list issues). - `ISSUE_PAGING_NUM`: **10**: Number of issues that are shown in one page (for all pages that list issues).
- `FEED_MAX_COMMIT_NUM`: **5**: Number of maximum commits shown in one activity feed. - `FEED_MAX_COMMIT_NUM`: **5**: Number of maximum commits shown in one activity feed.
- `GRAPH_MAX_COMMIT_NUM`: **100**: Number of maximum commits shown in the commit graph.
- `DEFAULT_THEME`: **gitea**: \[gitea, arc-green\]: Set the default theme for the Gitea install.
### UI - Admin (`ui.admin`) ### UI - Admin (`ui.admin`)
@ -115,6 +117,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `LFS_START_SERVER`: **false**: Enables git-lfs support. - `LFS_START_SERVER`: **false**: Enables git-lfs support.
- `LFS_CONTENT_PATH`: **./data/lfs**: Where to store LFS files. - `LFS_CONTENT_PATH`: **./data/lfs**: Where to store LFS files.
- `LFS_JWT_SECRET`: **\<empty\>**: LFS authentication secret, change this a unique string. - `LFS_JWT_SECRET`: **\<empty\>**: LFS authentication secret, change this a unique string.
- `LFS_HTTP_AUTH_EXPIRY`: **20m**: LFS authentication validity period in time.Duration, pushes taking longer than this may fail.
- `REDIRECT_OTHER_PORT`: **false**: If true and `PROTOCOL` is https, redirects http requests - `REDIRECT_OTHER_PORT`: **false**: If true and `PROTOCOL` is https, redirects http requests
on another (https) port. on another (https) port.
- `PORT_TO_REDIRECT`: **80**: Port used when `REDIRECT_OTHER_PORT` is true. - `PORT_TO_REDIRECT`: **80**: Port used when `REDIRECT_OTHER_PORT` is true.
@ -176,7 +179,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication. - `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication.
- `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration - `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration
for reverse authentication. for reverse authentication.
- `ENABLE_CAPTCHA`: **true**: Enable this to use captcha validation for registration. - `ENABLE_CAPTCHA`: **false**: Enable this to use captcha validation for registration.
- `CAPTCHA_TYPE`: **image**: \[image, recaptcha\]
- `RECAPTCHA_SECRET`: **""**: Go to https://www.google.com/recaptcha/admin to get a secret for recaptcha
- `RECAPTCHA_SITEKEY`: **""**: Go to https://www.google.com/recaptcha/admin to get a sitekey for recaptcha
- `DEFAULT_ENABLE_DEPENDENCIES`: **true** Enable this to have dependencies enabled by default.
## Webhook (`webhook`) ## Webhook (`webhook`)
@ -278,6 +285,13 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view. - `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view.
- `GC_ARGS`: **\<empty\>**: Arguments for command `git gc`, e.g. `--aggressive --auto`. - `GC_ARGS`: **\<empty\>**: Arguments for command `git gc`, e.g. `--aggressive --auto`.
## Git - Timeout settings (`git.timeout`)
- `MIGRATE`: **600**: Migrate external repositories timeout seconds.
- `MIRROR`: **300**: Mirror external repositories timeout seconds.
- `CLONE`: **300**: Git clone from internal repositories timeout seconds.
- `PULL`: **300**: Git pull from internal repositories timeout seconds.
- `GC`: **60**: Git repository GC timeout seconds.
## API (`api`) ## API (`api`)
- `ENABLE_SWAGGER_ENDPOINT`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true. - `ENABLE_SWAGGER_ENDPOINT`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.

View File

@ -187,6 +187,13 @@ menu:
- `MAX_GIT_DIFF_FILES`: 比较视图中的最大现实文件数目。 - `MAX_GIT_DIFF_FILES`: 比较视图中的最大现实文件数目。
- `GC_ARGS`: 执行 `git gc` 命令的参数, 比如: `--aggressive --auto` - `GC_ARGS`: 执行 `git gc` 命令的参数, 比如: `--aggressive --auto`
## Git - 超时设置 (`git.timeout`)
- `MIGRATE`: **600**: 迁移外部仓库时的超时时间,单位秒
- `MIRROR`: **300**: 镜像外部仓库的超时时间,单位秒
- `CLONE`: **300**: 内部仓库间克隆的超时时间,单位秒
- `PULL`: **300**: 内部仓库间拉取的超时时间,单位秒
- `GC`: **60**: git仓库GC的超时时间单位秒
## markup (`markup`) ## markup (`markup`)
外部渲染工具支持,你可以用你熟悉的文档渲染工具. 比如一下将新增一个名字为 `asciidoc` 的渲染工具which is followed `markup.` ini section. And there are some config items below. 外部渲染工具支持,你可以用你熟悉的文档渲染工具. 比如一下将新增一个名字为 `asciidoc` 的渲染工具which is followed `markup.` ini section. And there are some config items below.

View File

@ -91,3 +91,7 @@ Apart from `extra_links.tmpl` and `extra_tabs.tmpl`, there are other useful temp
## Customizing gitignores, labels, licenses, locales, and readmes. ## Customizing gitignores, labels, licenses, locales, and readmes.
Place custom files in corresponding sub-folder under `custom/options`. Place custom files in corresponding sub-folder under `custom/options`.
## Customizing the look of Gitea
Gitea has two built-in themes, the default theme `gitea`, and a dark theme `arc-green`. To change the look of your Gitea install change the value of `DEFAULT_THEME` in the [ui](https://docs.gitea.io/en-us/config-cheat-sheet/#ui-ui) section of `app.ini` to another one of the available options.

View File

@ -27,594 +27,93 @@ _Symbols used in table:_
* _✘ - unsupported_ * _✘ - unsupported_
<table border="1" cellpadding="4"> #### General Features
<thead>
<tr> | Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE |
<td>Feature</td> |---------|-------|------|-----------|-----------|-----------|-----------|--------------|
<td>Gitea</td> | Open source and free | ✓ | ✓ | ✘| ✓ | ✘ | ✘ | ✓ |
<td>Gogs</td> | Low resource usage (RAM/CPU) | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ |
<td>GitHub EE</td> | Multiple database support | ✓ | ✓ | ✘ | | | ✓ | ✓ |
<td>GitLab CE</td> | Multiple OS support | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ |
<td>GitLab EE</td> | Easy upgrade process | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ |
<td>BitBucket</td> | Markdown support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
</tr> | Static Git-powered pages | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
</thead> | Integrated Git-powered wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ |
<tbody> | Deploy Tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
<tr> | Repository Tokens with write rights | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ |
<td>Open source and free</td> | Built-in Container Registry | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
<td></td> | External git mirroring | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ |
<td></td> | FIDO U2F (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
<td></td> | Built-in CI/CD | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Subgroups: groups within groups | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ |
<td></td>
<td></td> #### Code management
</tr>
<tr> | Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE |
<td>Issue tracker</td> |---------|-------|------|-----------|-----------|-----------|-----------|--------------|
<td></td> | Repository topics | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Repository code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | Global code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | | ✓ |
<td></td> | Group Milestones | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Granular user roles (Code, Issues, Wiki etc) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
</tr> | Verified Committer | ✘ | ✘ | ? | ✓ | ✓ | ✓ | ✘ |
<tr> | GPG Signed Commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td>Pull/Merge requests</td> | Reject unsigned commits | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ |
<td></td> | Repository Activity page | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | Branch manager | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | Create new branches | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Web code editor | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | Commit graph | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td>
</tr> #### Issue Tracker
<tr>
<td>Squash merging</td> | Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE |
<td></td> |---------|-------|------|-----------|-----------|-----------|-----------|--------------|
<td></td> | Issue tracker | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ |
<td></td> | Issue templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Labels | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Time tracking | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Multiple assignees for issues | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
</tr> | Related issues | ✘ | ✘ | | ✘ | ✓ | ✘ | ✘ |
<tr> | Confidential issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
<td>Rebase merging</td> | Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Lock Discussion | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Issue Boards | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
<td></td> | Global issue search | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
</tr>
<tr> #### Pull/Merge requests
<td>Pull/Merge request inline comments</td>
<td></td> | Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE |
<td></td> |---------|-------|------|-----------|-----------|-----------|-----------|--------------|
<td></td> | Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | Squash merging | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ |
<td></td> | Rebase merging | ✓ | ✓ | ✓ | ✘ | | ✘ | ✓ |
<td></td> | Pull/Merge request inline comments | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
</tr> | Pull/Merge request approval | ✘ | ✘ | | ✓ | ✓ | ✓ | ✓ |
<tr> | Merge conflict resolution | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
<td>Pull/Merge request approval</td> | Restrict push and merge access to certain users | ✓ | ✘ | ✓ | | ✓ | ✓ | ✓ |
<td></td> | Revert specific commits or a merge request | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
<td></td> | Pull/Merge requests templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
<td></td> | Cherry-picking changes | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
<td></td>
<td></td>
<td></td> #### 3rd-party integrations
</tr>
<tr> | Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE |
<td>Merge conflict resolution</td> |---------|-------|------|-----------|-----------|-----------|-----------|--------------|
<td></td> | Webhook support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | Custom Git Hooks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | AD / LDAP integration | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | Multiple LDAP / AD server support | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ |
<td></td> | LDAP user synchronization | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td> | OpenId Connect support | ✓ | ✘ | ✓ | ✓ | ✓ | ? | ✘ |
</tr> | OAuth 2.0 integration (external authorization) | ✓ | ✘ | | ✓ | ✓ | ? | ✓ |
<tr> | Act as OAuth 2.0 provider | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
<td>Restrict push and merge access to certain users</td> | Two factor authentication (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ |
<td></td> | Mattermost/Slack integration | ✓ | ✓ | | ✓ | ✓ | | ✓ |
<td></td> | Discord integration | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ |
<td></td> | External CI/CD status display | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Markdown support</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Issues and pull/merge requests templates</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Revert specific commits or a merge request</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Labels</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Time tracking</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Multiple assignees for issues</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Related issues</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Confidential issues</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Comment reactions</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Lock Discussion</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Batch issue handling</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Issue Boards</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Create new branches from issues</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Commit graph</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Web code editor</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Branch manager</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Create new branches</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Repository topics</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Repository code search</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Global code search</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Issue search</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Global issue search</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Git LFS 2.0</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Integrated Git-powered wiki</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Static Git-powered pages</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Group Milestones</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Granular user roles (Code, Issues, Wiki etc)</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Cherry-picking changes</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>GPG Signed Commits</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Reject unsigned commits</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Verified Committer</td>
<td></td>
<td></td>
<td>?</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Subgroups: groups within groups</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Custom Git Hooks</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Repository Activity page</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Deploy Tokens</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Repository Tokens with write rights</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Easy upgrade process</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Built-in Container Registry</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>External git mirroring</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>AD / LDAP integration</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Multiple LDAP / AD server support</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>LDAP user synchronization</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>OpenId Connect support</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>?</td>
</tr>
<tr>
<td>OAuth 2.0 integration (external authorization)</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>?</td>
</tr>
<tr>
<td>Act as OAuth 2.0 provider</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Two factor authentication (2FA)</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>FIDO U2F (2FA)</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Webhook support</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Mattermost/Slack integration</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Discord integration</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Built-in CI/CD</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>External CI/CD status display</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Multiple database support</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Multiple OS support</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Low resource usage (RAM/CPU)</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>

View File

@ -20,11 +20,19 @@ embedded assets. This can be different for older releases. Choose the file match
the destination platform from the [downloads page](https://dl.gitea.io/gitea), copy the destination platform from the [downloads page](https://dl.gitea.io/gitea), copy
the URL and replace the URL within the commands below: the URL and replace the URL within the commands below:
``` ```sh
wget -O gitea https://dl.gitea.io/gitea/1.3.2/gitea-1.3.2-linux-amd64 wget -O gitea https://dl.gitea.io/gitea/1.4.3/gitea-1.4.3-linux-amd64
chmod +x gitea chmod +x gitea
``` ```
## Verify GPG signature
Gitea signs all binaries with a [GPG key](https://pgp.mit.edu/pks/lookup?op=vindex&fingerprint=on&search=0x2D9AE806EC1592E2) to prevent against unwanted modification of binaries. To validate the binary download the signature file which ends in `.asc` for the binary you downloaded and use the gpg command line tool.
```sh
gpg --keyserver pgp.mit.edu --recv 0x2D9AE806EC1592E2
gpg --verify gitea-1.5.0-linux-amd64.asc gitea-1.5.0-linux-amd64
```
## Test ## Test
After getting a binary, it can be tested with `./gitea web` or moved to a permanent After getting a binary, it can be tested with `./gitea web` or moved to a permanent
@ -34,6 +42,54 @@ location. When launched manually, Gitea can be killed using `Ctrl+C`.
./gitea web ./gitea web
``` ```
## Recommended server configuration
### Prepare environment
Check that git is installed on the server, if it is not install it first.
```sh
git --version
```
Create user to run gitea (ex. `git`)
```sh
adduser \
--system \
--shell /bin/bash \
--gecos 'Git Version Control' \
--group \
--disabled-password \
--home /home/git \
git
```
### Create required directory structure
```sh
mkdir -p /var/lib/gitea/{custom,data,indexers,public,log}
chown git:git /var/lib/gitea/{data,indexers,log}
chmod 750 /var/lib/gitea/{data,indexers,log}
mkdir /etc/gitea
chown root:git /etc/gitea
chmod 770 /etc/gitea
```
**NOTE:** `/etc/gitea` is temporary set with write rights for user `git` so that Web installer could write configuration file. After installation is done it is recommended to set rights to read-only using:
```
chmod 750 /etc/gitea
chmod 644 /etc/gitea/app.ini
```
### Copy gitea binary to global location
```
cp gitea /usr/local/bin/gitea
```
### Create service file to start gitea automatically
See how to create [Linux service]({{< relref "run-as-service-in-ubuntu.en-us.md" >}})
## Troubleshooting ## Troubleshooting
### Old glibc versions ### Old glibc versions

View File

@ -17,16 +17,13 @@ menu:
## Debian ## Debian
The only distribution that has any "official" package of Gitea is Debian. This is currently Although there is a package of Gitea in Debian's [contrib](https://wiki.debian.org/SourcesList),
in Debian's [contrib](https://wiki.debian.org/SourcesList). This is (currently) only available it is not supported directly by us.
in Debian testing and unstable (but should be installable/functional on stable).
- Edit /etc/apt/sourced.list Unfortunately the package is not maintained anymore and broken because of missing sources.
- Add "contrib" to "deb http://deb.debian.org/debian unstable main contrib" Please follow the [deployment from binary]({{< relref "from-binary.en-us.md" >}}) guide instead.
- apt-get update
- apt-get install gitea
For other distributions, see the [deployment from binary]({{< relref "from-binary.en-us.md" >}}) guide. Should the packages get updated and fixed, we will provide up-to-date installation instructions here.
## Windows ## Windows

View File

@ -231,7 +231,7 @@ You can configure some of Gitea's settings via environment variables:
* `APP_NAME`: **"Gitea: Git with a cup of tea"**: Application name, used in the page title. * `APP_NAME`: **"Gitea: Git with a cup of tea"**: Application name, used in the page title.
* `RUN_MODE`: **dev**: For performance and other purposes, change this to `prod` when deployed to a production environment. * `RUN_MODE`: **dev**: For performance and other purposes, change this to `prod` when deployed to a production environment.
* `SSH_DOMAIN`: **localhost**: Domain name of this server, used for displayed clone UR in Gitea's UI. * `SSH_DOMAIN`: **localhost**: Domain name of this server, used for the displayed clone URL in Gitea's UI.
* `SSH_PORT`: **22**: SSH port displayed in clone URL. * `SSH_PORT`: **22**: SSH port displayed in clone URL.
* `DISABLE_SSH`: **false**: Disable SSH feature when it's not available. * `DISABLE_SSH`: **false**: Disable SSH feature when it's not available.
* `HTTP_PORT`: **3000**: HTTP listen port. * `HTTP_PORT`: **3000**: HTTP listen port.
@ -245,6 +245,8 @@ You can configure some of Gitea's settings via environment variables:
* `SECRET_KEY`: **""**: Global secret key. This should be changed. If this has a value and `INSTALL_LOCK` is empty, `INSTALL_LOCK` will automatically set to `true`. * `SECRET_KEY`: **""**: Global secret key. This should be changed. If this has a value and `INSTALL_LOCK` is empty, `INSTALL_LOCK` will automatically set to `true`.
* `DISABLE_REGISTRATION`: **false**: Disable registration, after which only admin can create accounts for users. * `DISABLE_REGISTRATION`: **false**: Disable registration, after which only admin can create accounts for users.
* `REQUIRE_SIGNIN_VIEW`: **false**: Enable this to force users to log in to view any page. * `REQUIRE_SIGNIN_VIEW`: **false**: Enable this to force users to log in to view any page.
* `USER_UID`: **1000**: The UID (Unix user ID) of the user that runs Gitea within the container. Match this to the UID of the owner of the `/data` volume if using host volumes (this is not necessary with named volumes).
* `USER_GID`: **1000**: The GID (Unix group ID) of the user that runs Gitea within the container. Match this to the GID of the owner of the `/data` volume if using host volumes (this is not necessary with named volumes).
# Customization # Customization

View File

@ -20,7 +20,7 @@ file can be unpacked and used to restore an instance.
## Backup Command (`dump`) ## Backup Command (`dump`)
Switch to the user running gitea: `su git`. Run `./gitea dump` in the gitea installation Switch to the user running gitea: `su git`. Run `./gitea dump -c /path/to/app.ini` in the gitea installation
directory. There should be some output similar to the following: directory. There should be some output similar to the following:
``` ```
@ -34,7 +34,8 @@ directory. There should be some output similar to the following:
Inside the `gitea-dump-1482906742.zip` file, will be the following: Inside the `gitea-dump-1482906742.zip` file, will be the following:
* `custom/conf/app.ini` - Server config. * `custom` - All config or customerize files in `custom/`.
* `data` - Data directory in <GITEA_WORK_DIR>, except sessions if you are using file session. This directory includes `attachments`, `avatars`, `lfs`, `indexers`, sqlite file if you are using sqlite.
* `gitea-db.sql` - SQL dump of database * `gitea-db.sql` - SQL dump of database
* `gitea-repo.zip` - Complete copy of the repository directory. * `gitea-repo.zip` - Complete copy of the repository directory.
* `log/` - Various logs. They are not needed for a recovery or migration. * `log/` - Various logs. They are not needed for a recovery or migration.

View File

@ -0,0 +1,60 @@
---
date: "2018-06-06T09:33:00+08:00"
title: "使用: 备份与恢复"
slug: "backup-and-restore"
weight: 11
toc: true
draft: false
menu:
sidebar:
parent: "usage"
name: "备份与恢复"
weight: 11
identifier: "backup-and-restore"
---
# 备份与恢复
Gitea 已经实现了 `dump` 命令可以用来备份所有需要的文件到一个zip压缩文件。该压缩文件可以被用来进行数据恢复。
## 备份命令 (`dump`)
先转到git用户的权限: `su git`. 再Gitea目录运行 `./gitea dump`。一般会显示类似如下的输出:
```
2016/12/27 22:32:09 Creating tmp work dir: /tmp/gitea-dump-417443001
2016/12/27 22:32:09 Dumping local repositories.../home/git/gitea-repositories
2016/12/27 22:32:22 Dumping database...
2016/12/27 22:32:22 Packing dump files...
2016/12/27 22:32:34 Removing tmp work dir: /tmp/gitea-dump-417443001
2016/12/27 22:32:34 Finish dumping in file gitea-dump-1482906742.zip
```
最后生成的 `gitea-dump-1482906742.zip` 文件将会包含如下内容:
* `custom` - 所有保存在 `custom/` 目录下的配置和自定义的文件。
* `data` - 数据目录下的所有内容不包含使用文件session的文件。该目录包含 `attachments`, `avatars`, `lfs`, `indexers`, 如果使用sqlite 还会包含 sqlite 数据库文件。
* `gitea-db.sql` - 数据库dump出来的 SQL。
* `gitea-repo.zip` - Git仓库压缩文件。
* `log/` - Logs文件如果用作迁移不是必须的。
中间备份文件将会在临时目录进行创建,如果您要重新指定临时目录,可以用 `--tempdir` 参数,或者用 `TMPDIR` 环境变量。
## Restore Command (`restore`)
当前还没有恢复命令,恢复需要人工进行。主要是把文件和数据库进行恢复。
例如:
```
apt-get install gitea
unzip gitea-dump-1482906742.zip
cd gitea-dump-1482906742
mv custom/conf/app.ini /etc/gitea/conf/app.ini
unzip gitea-repo.zip
mv gitea-repo/* /var/lib/gitea/repositories/
chown -R gitea:gitea /etc/gitea/conf/app.ini /var/lib/gitea/repositories/
mysql -u$USER -p$PASS $DATABASE <gitea-db.sql
# or sqlite3 $DATABASE_PATH <gitea-db.sql
service gitea restart
```

View File

@ -62,6 +62,7 @@ Admin operations:
- Options: - Options:
- `--username value`, `-u value`: Username. Required. - `--username value`, `-u value`: Username. Required.
- `--password value`, `-p value`: New password. Required. - `--password value`, `-p value`: New password. Required.
- `--config path`: Gitea configuration file path. Optional. (default: custom/conf/app.ini).
- Examples: - Examples:
- `gitea admin change-password --username myname --password asecurepassword` - `gitea admin change-password --username myname --password asecurepassword`
- `regenerate` - `regenerate`

View File

@ -0,0 +1,46 @@
---
date: "2018-06-02T11:00:00+02:00"
title: "Usage: HTTPS setup"
slug: "https-setup"
weight: 12
toc: true
draft: false
menu:
sidebar:
parent: "usage"
name: "HTTPS setup"
weight: 12
identifier: "https-setup"
---
# HTTPS setup to encrypt connections to Gitea
## Using built-in server
Before you enable HTTPS make sure that you have valid SSL/TLS certificates.
You could use self-generated certificates for evaluation and testing. Please run `gitea cert --host [HOST]` to generate a self signed certificate.
To use Gitea's built-in HTTPS support you must change your `app.ini` file:
```ini
[server]
PROTOCOL=https
ROOT_URL = `https://git.example.com:3000/`
HTTP_PORT = 3000
CERT_FILE = cert.pem
KEY_FILE = key.pem
```
To learn more about the config values, please checkout the [Config Cheat Sheet](../config-cheat-sheet#server).
## Using reverse proxy
Setup up your reverse proxy like shown in the [reverse proxy guide](../reverse-proxies).
After that, enable HTTPS by following one of these guides:
* [nginx](https://nginx.org/en/docs/http/configuring_https_servers.html)
* [apache2/httpd](https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html)
* [caddy](https://caddyserver.com/docs/tls)
Note: You connection between your reverse proxy and gitea might be unencrypted. To encrypt it too follow the [built-in server guide](#using-built-in-server) and change
the proxy url to `https://[URL]`.

View File

@ -1,44 +0,0 @@
:80 {
root /srv/www
locale en-US zh-CN zh-TW pt-BR nl-NL fr-FR {
detect header
}
redir 301 {
if {path} match ^/$
/ /{>Detected-Locale}/
}
rewrite /en-US/ {
regexp (.*)
to /en-us/{1}
}
rewrite /zh-CN/ {
regexp (.*)
to /zh-cn/{1}
}
rewrite /zh-TW/ {
regexp (.*)
to /zh-tw/{1}
}
rewrite /pt-BR/ {
regexp (.*)
to /pt-br/{1}
}
rewrite /nl-NL/ {
regexp (.*)
to /nl-nl/{1}
}
rewrite /fr-FR/ {
regexp (.*)
to /fr-fr/{1}
}
header / Vary "Accept-Language"
}

View File

6
docs/static/_headers vendored Normal file
View File

@ -0,0 +1,6 @@
/*
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' https://fonts.googleapis.com https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com https://fonts.gstatic.com
X-Frame-Options: DENY
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin

8
docs/static/_redirects vendored Normal file
View File

@ -0,0 +1,8 @@
https://gitea-docs.netlify.com/* https://docs.gitea.io/:splat 302!
/ /fr-fr/ 302! Language=fr
/ /nl-nl/ 302! Language=nl
/ /pt-br/ 302! Language=pt-br
/ /zh-cn/ 302! Language=zh-cn
/ /zh-tw/ 302! Language=zh-tw
/ /en-us/ 302!

View File

@ -13,7 +13,7 @@ Make sure to perform a clean build before running tests:
## Run all tests via local drone ## Run all tests via local drone
``` ```
drone exec --local --build.event "pull_request" drone exec --local --build-event "pull_request"
``` ```
## Run sqlite integrations tests ## Run sqlite integrations tests

View File

@ -67,9 +67,9 @@ func TestAPISearchRepo(t *testing.T) {
expectedResults expectedResults
}{ }{
{name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50", expectedResults: expectedResults{ {name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50", expectedResults: expectedResults{
nil: {count: 15}, nil: {count: 16},
user: {count: 15}, user: {count: 16},
user2: {count: 15}}, user2: {count: 16}},
}, },
{name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{ {name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{
nil: {count: 10}, nil: {count: 10},
@ -235,3 +235,53 @@ func TestAPIGetRepoByIDUnauthorized(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repositories/2") req := NewRequestf(t, "GET", "/api/v1/repositories/2")
sess.MakeRequest(t, req, http.StatusNotFound) sess.MakeRequest(t, req, http.StatusNotFound)
} }
func TestAPIRepoMigrate(t *testing.T) {
testCases := []struct {
ctxUserID, userID int64
cloneURL, repoName string
expectedStatus int
}{
{ctxUserID: 1, userID: 2, cloneURL: "https://github.com/go-gitea/git.git", repoName: "git-admin", expectedStatus: http.StatusCreated},
{ctxUserID: 2, userID: 2, cloneURL: "https://github.com/go-gitea/git.git", repoName: "git-own", expectedStatus: http.StatusCreated},
{ctxUserID: 2, userID: 1, cloneURL: "https://github.com/go-gitea/git.git", repoName: "git-bad", expectedStatus: http.StatusForbidden},
{ctxUserID: 2, userID: 3, cloneURL: "https://github.com/go-gitea/git.git", repoName: "git-org", expectedStatus: http.StatusCreated},
{ctxUserID: 2, userID: 6, cloneURL: "https://github.com/go-gitea/git.git", repoName: "git-bad-org", expectedStatus: http.StatusForbidden},
}
prepareTestEnv(t)
for _, testCase := range testCases {
user := models.AssertExistsAndLoadBean(t, &models.User{ID: testCase.ctxUserID}).(*models.User)
session := loginUser(t, user.Name)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOption{
CloneAddr: testCase.cloneURL,
UID: int(testCase.userID),
RepoName: testCase.repoName,
})
session.MakeRequest(t, req, testCase.expectedStatus)
}
}
func TestAPIOrgRepoCreate(t *testing.T) {
testCases := []struct {
ctxUserID int64
orgName, repoName string
expectedStatus int
}{
{ctxUserID: 1, orgName: "user3", repoName: "repo-admin", expectedStatus: http.StatusCreated},
{ctxUserID: 2, orgName: "user3", repoName: "repo-own", expectedStatus: http.StatusCreated},
{ctxUserID: 2, orgName: "user6", repoName: "repo-bad-org", expectedStatus: http.StatusForbidden},
}
prepareTestEnv(t)
for _, testCase := range testCases {
user := models.AssertExistsAndLoadBean(t, &models.User{ID: testCase.ctxUserID}).(*models.User)
session := loginUser(t, user.Name)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", testCase.orgName), &api.CreateRepoOption{
Name: testCase.repoName,
})
session.MakeRequest(t, req, testCase.expectedStatus)
}
}

View File

@ -0,0 +1,50 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integrations
import (
"net/http"
"testing"
"code.gitea.io/gitea/models"
api "code.gitea.io/sdk/gitea"
)
// TestAPICreateAndDeleteToken tests that token that was just created can be deleted
func TestAPICreateAndDeleteToken(t *testing.T) {
prepareTestEnv(t)
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
req := NewRequestWithJSON(t, "POST", "/api/v1/users/user1/tokens", map[string]string{
"name": "test-key-1",
})
req = AddBasicAuthHeader(req, user.Name)
resp := MakeRequest(t, req, http.StatusCreated)
var newAccessToken api.AccessToken
DecodeJSON(t, resp, &newAccessToken)
models.AssertExistsAndLoadBean(t, &models.AccessToken{
ID: newAccessToken.ID,
Name: newAccessToken.Name,
Sha1: newAccessToken.Sha1,
UID: user.ID,
})
req = NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", newAccessToken.ID)
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusNoContent)
models.AssertNotExistsBean(t, &models.AccessToken{ID: newAccessToken.ID})
}
// TestAPIDeleteMissingToken ensures that error is thrown when token not found
func TestAPIDeleteMissingToken(t *testing.T) {
prepareTestEnv(t)
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
req := NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", models.NonexistentID)
req = AddBasicAuthHeader(req, user.Name)
MakeRequest(t, req, http.StatusNotFound)
}

View File

@ -256,6 +256,11 @@ func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *ht
return request return request
} }
func AddBasicAuthHeader(request *http.Request, username string) *http.Request {
request.SetBasicAuth(username, userPassword)
return request
}
const NoExpectedStatus = -1 const NoExpectedStatus = -1
func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *httptest.ResponseRecorder { func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *httptest.ResponseRecorder {

View File

@ -68,3 +68,25 @@ func TestSettingShowUserEmailProfile(t *testing.T) {
setting.UI.ShowUserEmail = showUserEmail setting.UI.ShowUserEmail = showUserEmail
} }
func TestSettingLandingPage(t *testing.T) {
prepareTestEnv(t)
landingPage := setting.LandingPageURL
setting.LandingPageURL = setting.LandingPageHome
req := NewRequest(t, "GET", "/")
MakeRequest(t, req, http.StatusOK)
setting.LandingPageURL = setting.LandingPageExplore
req = NewRequest(t, "GET", "/")
resp := MakeRequest(t, req, http.StatusFound)
assert.Equal(t, "/explore", resp.Header().Get("Location"))
setting.LandingPageURL = setting.LandingPageOrganizations
req = NewRequest(t, "GET", "/")
resp = MakeRequest(t, req, http.StatusFound)
assert.Equal(t, "/explore/organizations", resp.Header().Get("Location"))
setting.LandingPageURL = landingPage
}

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
// register supported doc types // register supported doc types
_ "code.gitea.io/gitea/modules/markup/csv"
_ "code.gitea.io/gitea/modules/markup/markdown" _ "code.gitea.io/gitea/modules/markup/markdown"
_ "code.gitea.io/gitea/modules/markup/orgmode" _ "code.gitea.io/gitea/modules/markup/orgmode"

View File

@ -22,8 +22,12 @@ func TestAccessLevel(t *testing.T) {
user1 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) user1 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
user2 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) user2 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User)
repo1 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 2, IsPrivate: false}).(*Repository) // A public repository owned by User 2
repo2 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 3, IsPrivate: true}).(*Repository) repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
assert.False(t, repo1.IsPrivate)
// A private repository owned by Org 3
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
assert.True(t, repo2.IsPrivate)
level, err := AccessLevel(user1.ID, repo1) level, err := AccessLevel(user1.ID, repo1)
assert.NoError(t, err) assert.NoError(t, err)
@ -47,8 +51,12 @@ func TestHasAccess(t *testing.T) {
user1 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) user1 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
user2 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) user2 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User)
repo1 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 2, IsPrivate: false}).(*Repository) // A public repository owned by User 2
repo2 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 3, IsPrivate: true}).(*Repository) repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
assert.False(t, repo1.IsPrivate)
// A private repository owned by Org 3
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
assert.True(t, repo2.IsPrivate)
for _, accessMode := range accessModes { for _, accessMode := range accessModes {
has, err := HasAccess(user1.ID, repo1, accessMode) has, err := HasAccess(user1.ID, repo1, accessMode)

View File

@ -122,6 +122,12 @@ func (a *Action) loadRepo() {
} }
} }
// GetActFullName gets the action's user full name.
func (a *Action) GetActFullName() string {
a.loadActUser()
return a.ActUser.FullName
}
// GetActUserName gets the action's user name. // GetActUserName gets the action's user name.
func (a *Action) GetActUserName() string { func (a *Action) GetActUserName() string {
a.loadActUser() a.loadActUser()
@ -471,6 +477,10 @@ func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) err
} }
if err = issue.ChangeStatus(doer, repo, true); err != nil { if err = issue.ChangeStatus(doer, repo, true); err != nil {
// Don't return an error when dependencies are open as this would let the push fail
if IsErrDependenciesLeft(err) {
return nil
}
return err return err
} }
} }

View File

@ -102,7 +102,7 @@ func IsErrEmailAlreadyUsed(err error) bool {
} }
func (err ErrEmailAlreadyUsed) Error() string { func (err ErrEmailAlreadyUsed) Error() string {
return fmt.Sprintf("e-mail has been used [email: %s]", err.Email) return fmt.Sprintf("e-mail already in use [email: %s]", err.Email)
} }
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error. // ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
@ -117,7 +117,7 @@ func IsErrOpenIDAlreadyUsed(err error) bool {
} }
func (err ErrOpenIDAlreadyUsed) Error() string { func (err ErrOpenIDAlreadyUsed) Error() string {
return fmt.Sprintf("OpenID has been used [oid: %s]", err.OpenID) return fmt.Sprintf("OpenID already in use [oid: %s]", err.OpenID)
} }
// ErrUserOwnRepos represents a "UserOwnRepos" kind of error. // ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
@ -1259,3 +1259,88 @@ func IsErrU2FRegistrationNotExist(err error) bool {
_, ok := err.(ErrU2FRegistrationNotExist) _, ok := err.(ErrU2FRegistrationNotExist)
return ok return ok
} }
// .___ ________ .___ .__
// | | ______ ________ __ ____ \______ \ ____ ______ ____ ____ __| _/____ ____ ____ |__| ____ ______
// | |/ ___// ___/ | \_/ __ \ | | \_/ __ \\____ \_/ __ \ / \ / __ |/ __ \ / \_/ ___\| |/ __ \ / ___/
// | |\___ \ \___ \| | /\ ___/ | ` \ ___/| |_> > ___/| | \/ /_/ \ ___/| | \ \___| \ ___/ \___ \
// |___/____ >____ >____/ \___ >_______ /\___ > __/ \___ >___| /\____ |\___ >___| /\___ >__|\___ >____ >
// \/ \/ \/ \/ \/|__| \/ \/ \/ \/ \/ \/ \/ \/
// ErrDependencyExists represents a "DependencyAlreadyExists" kind of error.
type ErrDependencyExists struct {
IssueID int64
DependencyID int64
}
// IsErrDependencyExists checks if an error is a ErrDependencyExists.
func IsErrDependencyExists(err error) bool {
_, ok := err.(ErrDependencyExists)
return ok
}
func (err ErrDependencyExists) Error() string {
return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
}
// ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error.
type ErrDependencyNotExists struct {
IssueID int64
DependencyID int64
}
// IsErrDependencyNotExists checks if an error is a ErrDependencyExists.
func IsErrDependencyNotExists(err error) bool {
_, ok := err.(ErrDependencyNotExists)
return ok
}
func (err ErrDependencyNotExists) Error() string {
return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
}
// ErrCircularDependency represents a "DependencyCircular" kind of error.
type ErrCircularDependency struct {
IssueID int64
DependencyID int64
}
// IsErrCircularDependency checks if an error is a ErrCircularDependency.
func IsErrCircularDependency(err error) bool {
_, ok := err.(ErrCircularDependency)
return ok
}
func (err ErrCircularDependency) Error() string {
return fmt.Sprintf("circular dependencies exists (two issues blocking each other) [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
}
// ErrDependenciesLeft represents an error where the issue you're trying to close still has dependencies left.
type ErrDependenciesLeft struct {
IssueID int64
}
// IsErrDependenciesLeft checks if an error is a ErrDependenciesLeft.
func IsErrDependenciesLeft(err error) bool {
_, ok := err.(ErrDependenciesLeft)
return ok
}
func (err ErrDependenciesLeft) Error() string {
return fmt.Sprintf("issue has open dependencies [issue id: %d]", err.IssueID)
}
// ErrUnknownDependencyType represents an error where an unknown dependency type was passed
type ErrUnknownDependencyType struct {
Type DependencyType
}
// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
func IsErrUnknownDependencyType(err error) bool {
_, ok := err.(ErrUnknownDependencyType)
return ok
}
func (err ErrUnknownDependencyType) Error() string {
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
}

View File

@ -351,7 +351,7 @@
is_mirror: true is_mirror: true
num_forks: 1 num_forks: 1
is_fork: false is_fork: false
- -
id: 29 id: 29
fork_id: 27 fork_id: 27
@ -365,7 +365,7 @@
num_closed_pulls: 0 num_closed_pulls: 0
is_mirror: false is_mirror: false
is_fork: true is_fork: true
- -
id: 30 id: 30
fork_id: 28 fork_id: 28
@ -389,3 +389,14 @@
num_forks: 0 num_forks: 0
num_issues: 0 num_issues: 0
is_mirror: false is_mirror: false
-
id: 32
owner_id: 3
lower_name: repo21
name: repo21
is_private: false
num_stars: 0
num_forks: 0
num_issues: 0
is_mirror: false

View File

@ -4,9 +4,8 @@
lower_name: owners lower_name: owners
name: Owners name: Owners
authorize: 4 # owner authorize: 4 # owner
num_repos: 2 num_repos: 3
num_members: 1 num_members: 1
unit_types: '[1,2,3,4,5,6,7]'
- -
id: 2 id: 2
@ -16,7 +15,6 @@
authorize: 2 # write authorize: 2 # write
num_repos: 1 num_repos: 1
num_members: 2 num_members: 2
unit_types: '[1,2,3,4,5,6,7]'
- -
id: 3 id: 3
@ -26,7 +24,6 @@
authorize: 4 # owner authorize: 4 # owner
num_repos: 0 num_repos: 0
num_members: 1 num_members: 1
unit_types: '[1,2,3,4,5,6,7]'
- -
id: 4 id: 4
@ -36,7 +33,6 @@
authorize: 4 # owner authorize: 4 # owner
num_repos: 0 num_repos: 0
num_members: 1 num_members: 1
unit_types: '[1,2,3,4,5,6,7]'
- -
id: 5 id: 5
@ -46,7 +42,6 @@
authorize: 4 # owner authorize: 4 # owner
num_repos: 2 num_repos: 2
num_members: 2 num_members: 2
unit_types: '[1,2,3,4,5,6,7]'
- -
id: 6 id: 6
@ -56,4 +51,3 @@
authorize: 4 # owner authorize: 4 # owner
num_repos: 2 num_repos: 2
num_members: 1 num_members: 1
unit_types: '[1,2,3,4,5,6,7]'

View File

@ -33,9 +33,15 @@
org_id: 19 org_id: 19
team_id: 6 team_id: 6
repo_id: 27 repo_id: 27
- -
id: 7 id: 7
org_id: 19 org_id: 19
team_id: 6 team_id: 6
repo_id: 28 repo_id: 28
-
id: 8
org_id: 3
team_id: 1
repo_id: 32

View File

@ -0,0 +1,209 @@
-
id: 1
team_id: 1
type: 1
-
id: 2
team_id: 1
type: 2
-
id: 3
team_id: 1
type: 3
-
id: 4
team_id: 1
type: 4
-
id: 5
team_id: 1
type: 5
-
id: 6
team_id: 1
type: 6
-
id: 7
team_id: 1
type: 7
-
id: 8
team_id: 2
type: 1
-
id: 9
team_id: 2
type: 2
-
id: 10
team_id: 2
type: 3
-
id: 11
team_id: 2
type: 4
-
id: 12
team_id: 2
type: 5
-
id: 13
team_id: 2
type: 6
-
id: 14
team_id: 2
type: 7
-
id: 15
team_id: 3
type: 1
-
id: 16
team_id: 3
type: 2
-
id: 17
team_id: 3
type: 3
-
id: 18
team_id: 3
type: 4
-
id: 19
team_id: 3
type: 5
-
id: 20
team_id: 3
type: 6
-
id: 21
team_id: 3
type: 7
-
id: 22
team_id: 4
type: 1
-
id: 23
team_id: 4
type: 2
-
id: 24
team_id: 4
type: 3
-
id: 25
team_id: 4
type: 4
-
id: 26
team_id: 4
type: 5
-
id: 27
team_id: 4
type: 6
-
id: 28
team_id: 4
type: 7
-
id: 29
team_id: 5
type: 1
-
id: 30
team_id: 5
type: 2
-
id: 31
team_id: 5
type: 3
-
id: 32
team_id: 5
type: 4
-
id: 33
team_id: 5
type: 5
-
id: 34
team_id: 5
type: 6
-
id: 35
team_id: 5
type: 7
-
id: 36
team_id: 6
type: 1
-
id: 37
team_id: 6
type: 2
-
id: 38
team_id: 6
type: 3
-
id: 39
team_id: 6
type: 4
-
id: 40
team_id: 6
type: 5
-
id: 41
team_id: 6
type: 6
-
id: 42
team_id: 6
type: 7

View File

@ -45,7 +45,7 @@
is_admin: false is_admin: false
avatar: avatar3 avatar: avatar3
avatar_email: user3@example.com avatar_email: user3@example.com
num_repos: 2 num_repos: 3
num_members: 2 num_members: 2
num_teams: 2 num_teams: 2

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"code.gitea.io/git" "code.gitea.io/git"
"code.gitea.io/gitea/modules/setting"
) )
// GraphItem represent one commit, or one relation in timeline // GraphItem represent one commit, or one relation in timeline
@ -41,7 +42,7 @@ func GetCommitGraph(r *git.Repository) (GraphItems, error) {
"--all", "--all",
"-C", "-C",
"-M", "-M",
"-n 100", fmt.Sprintf("-n %d", setting.UI.GraphMaxCommitNum),
"--date=iso", "--date=iso",
fmt.Sprintf("--pretty=format:%s", format), fmt.Sprintf("--pretty=format:%s", format),
) )
@ -66,7 +67,7 @@ func graphItemFromString(s string, r *git.Repository) (GraphItem, error) {
var ascii string var ascii string
var data = "|||||||" var data = "|||||||"
lines := strings.Split(s, "DATA:") lines := strings.SplitN(s, "DATA:", 2)
switch len(lines) { switch len(lines) {
case 1: case 1:

View File

@ -5,6 +5,7 @@
package models package models
import ( import (
"fmt"
"testing" "testing"
"code.gitea.io/git" "code.gitea.io/git"
@ -43,3 +44,32 @@ func BenchmarkParseCommitString(b *testing.B) {
} }
} }
} }
func TestCommitStringParsing(t *testing.T) {
dataFirstPart := "* DATA:||4e61bacab44e9b4730e44a6615d04098dd3a8eaf|2016-12-20 21:10:41 +0100|Author|user@mail.something|4e61bac|"
tests := []struct {
shouldPass bool
testName string
commitMessage string
}{
{true, "normal", "not a fancy message"},
{true, "extra pipe", "An extra pipe: |"},
{true, "extra 'Data:'", "DATA: might be trouble"},
}
for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
testString := fmt.Sprintf("%s%s", dataFirstPart, test.commitMessage)
graphItem, err := graphItemFromString(testString, nil)
if err != nil && test.shouldPass {
t.Errorf("Could not parse %s", testString)
return
}
if test.commitMessage != graphItem.Subject {
t.Errorf("%s does not match %s", test.commitMessage, graphItem.Subject)
}
})
}
}

View File

@ -649,6 +649,20 @@ func (issue *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository,
if issue.IsClosed == isClosed { if issue.IsClosed == isClosed {
return nil return nil
} }
// Check for open dependencies
if isClosed && issue.Repo.IsDependenciesEnabled() {
// only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies
noDeps, err := IssueNoDependenciesLeft(issue)
if err != nil {
return err
}
if !noDeps {
return ErrDependenciesLeft{issue.ID}
}
}
issue.IsClosed = isClosed issue.IsClosed = isClosed
if isClosed { if isClosed {
issue.ClosedUnix = util.TimeStampNow() issue.ClosedUnix = util.TimeStampNow()
@ -1283,7 +1297,7 @@ func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
And("`comment`.type = ?", CommentTypeComment). And("`comment`.type = ?", CommentTypeComment).
And("`user`.is_active = ?", true). And("`user`.is_active = ?", true).
And("`user`.prohibit_login = ?", false). And("`user`.prohibit_login = ?", false).
Join("INNER", "user", "`user`.id = `comment`.poster_id"). Join("INNER", "`user`", "`user`.id = `comment`.poster_id").
Distinct("poster_id"). Distinct("poster_id").
Find(&userIDs); err != nil { Find(&userIDs); err != nil {
return nil, fmt.Errorf("get poster IDs: %v", err) return nil, fmt.Errorf("get poster IDs: %v", err)
@ -1598,3 +1612,33 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix util.TimeStamp, doer *User)
return sess.Commit() return sess.Commit()
} }
// Get Blocked By Dependencies, aka all issues this issue is blocked by.
func (issue *Issue) getBlockedByDependencies(e Engine) (issueDeps []*Issue, err error) {
return issueDeps, e.
Table("issue_dependency").
Select("issue.*").
Join("INNER", "issue", "issue.id = issue_dependency.dependency_id").
Where("issue_id = ?", issue.ID).
Find(&issueDeps)
}
// Get Blocking Dependencies, aka all issues this issue blocks.
func (issue *Issue) getBlockingDependencies(e Engine) (issueDeps []*Issue, err error) {
return issueDeps, e.
Table("issue_dependency").
Select("issue.*").
Join("INNER", "issue", "issue.id = issue_dependency.issue_id").
Where("dependency_id = ?", issue.ID).
Find(&issueDeps)
}
// BlockedByDependencies finds all Dependencies an issue is blocked by
func (issue *Issue) BlockedByDependencies() ([]*Issue, error) {
return issue.getBlockedByDependencies(x)
}
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
func (issue *Issue) BlockingDependencies() ([]*Issue, error) {
return issue.getBlockingDependencies(x)
}

View File

@ -66,6 +66,10 @@ const (
CommentTypeModifiedDeadline CommentTypeModifiedDeadline
// Removed a due date // Removed a due date
CommentTypeRemovedDeadline CommentTypeRemovedDeadline
// Dependency added
CommentTypeAddDependency
//Dependency removed
CommentTypeRemoveDependency
) )
// CommentTag defines comment tag type // CommentTag defines comment tag type
@ -81,23 +85,25 @@ const (
// Comment represents a comment in commit and issue page. // Comment represents a comment in commit and issue page.
type Comment struct { type Comment struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
Type CommentType Type CommentType
PosterID int64 `xorm:"INDEX"` PosterID int64 `xorm:"INDEX"`
Poster *User `xorm:"-"` Poster *User `xorm:"-"`
IssueID int64 `xorm:"INDEX"` IssueID int64 `xorm:"INDEX"`
Issue *Issue `xorm:"-"` Issue *Issue `xorm:"-"`
LabelID int64 LabelID int64
Label *Label `xorm:"-"` Label *Label `xorm:"-"`
OldMilestoneID int64 OldMilestoneID int64
MilestoneID int64 MilestoneID int64
OldMilestone *Milestone `xorm:"-"` OldMilestone *Milestone `xorm:"-"`
Milestone *Milestone `xorm:"-"` Milestone *Milestone `xorm:"-"`
AssigneeID int64 AssigneeID int64
RemovedAssignee bool RemovedAssignee bool
Assignee *User `xorm:"-"` Assignee *User `xorm:"-"`
OldTitle string OldTitle string
NewTitle string NewTitle string
DependentIssueID int64
DependentIssue *Issue `xorm:"-"`
CommitID int64 CommitID int64
Line int64 Line int64
@ -147,6 +153,10 @@ func (c *Comment) AfterLoad(session *xorm.Session) {
// AfterDelete is invoked from XORM after the object is deleted. // AfterDelete is invoked from XORM after the object is deleted.
func (c *Comment) AfterDelete() { func (c *Comment) AfterDelete() {
if c.ID <= 0 {
return
}
_, err := DeleteAttachmentsByComment(c.ID, true) _, err := DeleteAttachmentsByComment(c.ID, true)
if err != nil { if err != nil {
@ -277,6 +287,15 @@ func (c *Comment) LoadAssigneeUser() error {
return nil return nil
} }
// LoadDepIssueDetails loads Dependent Issue Details
func (c *Comment) LoadDepIssueDetails() (err error) {
if c.DependentIssueID <= 0 || c.DependentIssue != nil {
return nil
}
c.DependentIssue, err = getIssueByID(x, c.DependentIssueID)
return err
}
// MailParticipants sends new comment emails to repository watchers // MailParticipants sends new comment emails to repository watchers
// and mentioned people. // and mentioned people.
func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) { func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
@ -328,22 +347,24 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
if opts.Label != nil { if opts.Label != nil {
LabelID = opts.Label.ID LabelID = opts.Label.ID
} }
comment := &Comment{ comment := &Comment{
Type: opts.Type, Type: opts.Type,
PosterID: opts.Doer.ID, PosterID: opts.Doer.ID,
Poster: opts.Doer, Poster: opts.Doer,
IssueID: opts.Issue.ID, IssueID: opts.Issue.ID,
LabelID: LabelID, LabelID: LabelID,
OldMilestoneID: opts.OldMilestoneID, OldMilestoneID: opts.OldMilestoneID,
MilestoneID: opts.MilestoneID, MilestoneID: opts.MilestoneID,
RemovedAssignee: opts.RemovedAssignee, RemovedAssignee: opts.RemovedAssignee,
AssigneeID: opts.AssigneeID, AssigneeID: opts.AssigneeID,
CommitID: opts.CommitID, CommitID: opts.CommitID,
CommitSHA: opts.CommitSHA, CommitSHA: opts.CommitSHA,
Line: opts.LineNum, Line: opts.LineNum,
Content: opts.Content, Content: opts.Content,
OldTitle: opts.OldTitle, OldTitle: opts.OldTitle,
NewTitle: opts.NewTitle, NewTitle: opts.NewTitle,
DependentIssueID: opts.DependentIssueID,
} }
if _, err = e.Insert(comment); err != nil { if _, err = e.Insert(comment); err != nil {
return nil, err return nil, err
@ -545,6 +566,39 @@ func createDeleteBranchComment(e *xorm.Session, doer *User, repo *Repository, is
}) })
} }
// Creates issue dependency comment
func createIssueDependencyComment(e *xorm.Session, doer *User, issue *Issue, dependentIssue *Issue, add bool) (err error) {
cType := CommentTypeAddDependency
if !add {
cType = CommentTypeRemoveDependency
}
// Make two comments, one in each issue
_, err = createComment(e, &CreateCommentOptions{
Type: cType,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
DependentIssueID: dependentIssue.ID,
})
if err != nil {
return
}
_, err = createComment(e, &CreateCommentOptions{
Type: cType,
Doer: doer,
Repo: issue.Repo,
Issue: dependentIssue,
DependentIssueID: issue.ID,
})
if err != nil {
return
}
return
}
// CreateCommentOptions defines options for creating comment // CreateCommentOptions defines options for creating comment
type CreateCommentOptions struct { type CreateCommentOptions struct {
Type CommentType Type CommentType
@ -553,17 +607,18 @@ type CreateCommentOptions struct {
Issue *Issue Issue *Issue
Label *Label Label *Label
OldMilestoneID int64 DependentIssueID int64
MilestoneID int64 OldMilestoneID int64
AssigneeID int64 MilestoneID int64
RemovedAssignee bool AssigneeID int64
OldTitle string RemovedAssignee bool
NewTitle string OldTitle string
CommitID int64 NewTitle string
CommitSHA string CommitID int64
LineNum int64 CommitSHA string
Content string LineNum int64
Attachments []string // UUIDs of attachments Content string
Attachments []string // UUIDs of attachments
} }
// CreateComment creates comment of issue or commit. // CreateComment creates comment of issue or commit.

137
models/issue_dependency.go Normal file
View File

@ -0,0 +1,137 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// IssueDependency represents an issue dependency
type IssueDependency struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
IssueID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"`
DependencyID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"`
CreatedUnix util.TimeStamp `xorm:"created"`
UpdatedUnix util.TimeStamp `xorm:"updated"`
}
// DependencyType Defines Dependency Type Constants
type DependencyType int
// Define Dependency Types
const (
DependencyTypeBlockedBy DependencyType = iota
DependencyTypeBlocking
)
// CreateIssueDependency creates a new dependency for an issue
func CreateIssueDependency(user *User, issue, dep *Issue) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
// Check if it aleready exists
exists, err := issueDepExists(sess, issue.ID, dep.ID)
if err != nil {
return err
}
if exists {
return ErrDependencyExists{issue.ID, dep.ID}
}
// And if it would be circular
circular, err := issueDepExists(sess, dep.ID, issue.ID)
if err != nil {
return err
}
if circular {
return ErrCircularDependency{issue.ID, dep.ID}
}
if _, err := sess.Insert(&IssueDependency{
UserID: user.ID,
IssueID: issue.ID,
DependencyID: dep.ID,
}); err != nil {
return err
}
// Add comment referencing the new dependency
if err = createIssueDependencyComment(sess, user, issue, dep, true); err != nil {
return err
}
return sess.Commit()
}
// RemoveIssueDependency removes a dependency from an issue
func RemoveIssueDependency(user *User, issue *Issue, dep *Issue, depType DependencyType) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
var issueDepToDelete IssueDependency
switch depType {
case DependencyTypeBlockedBy:
issueDepToDelete = IssueDependency{IssueID: issue.ID, DependencyID: dep.ID}
case DependencyTypeBlocking:
issueDepToDelete = IssueDependency{IssueID: dep.ID, DependencyID: issue.ID}
default:
return ErrUnknownDependencyType{depType}
}
affected, err := sess.Delete(&issueDepToDelete)
if err != nil {
return err
}
// If we deleted nothing, the dependency did not exist
if affected <= 0 {
return ErrDependencyNotExists{issue.ID, dep.ID}
}
// Add comment referencing the removed dependency
if err = createIssueDependencyComment(sess, user, issue, dep, false); err != nil {
return err
}
return sess.Commit()
}
// Check if the dependency already exists
func issueDepExists(e Engine, issueID int64, depID int64) (bool, error) {
return e.Where("(issue_id = ? AND dependency_id = ?)", issueID, depID).Exist(&IssueDependency{})
}
// IssueNoDependenciesLeft checks if issue can be closed
func IssueNoDependenciesLeft(issue *Issue) (bool, error) {
exists, err := x.
Table("issue_dependency").
Select("issue.*").
Join("INNER", "issue", "issue.id = issue_dependency.dependency_id").
Where("issue_dependency.issue_id = ?", issue.ID).
And("issue.is_closed = ?", "0").
Exist(&Issue{})
return !exists, err
}
// IsDependenciesEnabled returns if dependecies are enabled and returns the default setting if not set.
func (repo *Repository) IsDependenciesEnabled() bool {
var u *RepoUnit
var err error
if u, err = repo.GetUnit(UnitTypeIssues); err != nil {
log.Trace("%s", err)
return setting.Service.DefaultEnableDependencies
}
return u.IssuesConfig().EnableDependencies
}

View File

@ -0,0 +1,57 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateIssueDependency(t *testing.T) {
// Prepare
assert.NoError(t, PrepareTestDatabase())
user1, err := GetUserByID(1)
assert.NoError(t, err)
issue1, err := GetIssueByID(1)
assert.NoError(t, err)
issue2, err := GetIssueByID(2)
assert.NoError(t, err)
// Create a dependency and check if it was successful
err = CreateIssueDependency(user1, issue1, issue2)
assert.NoError(t, err)
// Do it again to see if it will check if the dependency already exists
err = CreateIssueDependency(user1, issue1, issue2)
assert.Error(t, err)
assert.True(t, IsErrDependencyExists(err))
// Check for circular dependencies
err = CreateIssueDependency(user1, issue2, issue1)
assert.Error(t, err)
assert.True(t, IsErrCircularDependency(err))
_ = AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID})
// Check if dependencies left is correct
left, err := IssueNoDependenciesLeft(issue1)
assert.NoError(t, err)
assert.False(t, left)
// Close #2 and check again
err = issue2.ChangeStatus(user1, issue2.Repo, true)
assert.NoError(t, err)
left, err = IssueNoDependenciesLeft(issue1)
assert.NoError(t, err)
assert.True(t, left)
// Test removing the dependency
err = RemoveIssueDependency(user1, issue1, issue2, DependencyTypeBlockedBy)
assert.NoError(t, err)
}

View File

@ -166,7 +166,7 @@ func (issues IssueList) loadAssignees(e Engine) error {
var assignees = make(map[int64][]*User, len(issues)) var assignees = make(map[int64][]*User, len(issues))
rows, err := e.Table("issue_assignees"). rows, err := e.Table("issue_assignees").
Join("INNER", "user", "`user`.id = `issue_assignees`.assignee_id"). Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
In("`issue_assignees`.issue_id", issues.getIssueIDs()). In("`issue_assignees`.issue_id", issues.getIssueIDs()).
Rows(new(AssigneeIssue)) Rows(new(AssigneeIssue))
if err != nil { if err != nil {

View File

@ -67,7 +67,19 @@ func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error
Where("`issue_watch`.issue_id = ?", issueID). Where("`issue_watch`.issue_id = ?", issueID).
And("`user`.is_active = ?", true). And("`user`.is_active = ?", true).
And("`user`.prohibit_login = ?", false). And("`user`.prohibit_login = ?", false).
Join("INNER", "user", "`user`.id = `issue_watch`.user_id"). Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id").
Find(&watches) Find(&watches)
return return
} }
func removeIssueWatchersByRepoID(e Engine, userID int64, repoID int64) error {
iw := &IssueWatch{
IsWatching: false,
}
_, err := e.
Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID).
Cols("is_watching", "updated_unix").
Where("`issue_watch`.user_id = ?", userID).
Update(iw)
return err
}

View File

@ -187,6 +187,14 @@ var migrations = []Migration{
// v66 -> v67 // v66 -> v67
NewMigration("add login source id column for public_key table", addLoginSourceIDToPublicKeyTable), NewMigration("add login source id column for public_key table", addLoginSourceIDToPublicKeyTable),
// v67 -> v68 // v67 -> v68
NewMigration("remove stale watches", removeStaleWatches),
// v68 -> V69
NewMigration("Reformat and remove incorrect topics", reformatAndRemoveIncorrectTopics),
// v69 -> v70
NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
// v70 -> v71
NewMigration("add issue_dependencies", addIssueDependencies),
// v71 -> v72
NewMigration("add pull request rebase with merge commit", addPullRequestRebaseWithMergeCommit), NewMigration("add pull request rebase with merge commit", addPullRequestRebaseWithMergeCommit),
} }

View File

@ -25,10 +25,15 @@ func removeCommitsUnitType(x *xorm.Engine) (err error) {
Created time.Time `xorm:"-"` Created time.Time `xorm:"-"`
} }
type Team struct {
ID int64
UnitTypes []int `xorm:"json"`
}
// Update team unit types // Update team unit types
const batchSize = 100 const batchSize = 100
for start := 0; ; start += batchSize { for start := 0; ; start += batchSize {
teams := make([]*models.Team, 0, batchSize) teams := make([]*Team, 0, batchSize)
if err := x.Limit(batchSize, start).Find(&teams); err != nil { if err := x.Limit(batchSize, start).Find(&teams); err != nil {
return err return err
} }
@ -36,7 +41,7 @@ func removeCommitsUnitType(x *xorm.Engine) (err error) {
break break
} }
for _, team := range teams { for _, team := range teams {
ut := make([]models.UnitType, 0, len(team.UnitTypes)) ut := make([]int, 0, len(team.UnitTypes))
for _, u := range team.UnitTypes { for _, u := range team.UnitTypes {
if u < V16UnitTypeCommits { if u < V16UnitTypeCommits {
ut = append(ut, u) ut = append(ut, u)

View File

@ -34,7 +34,7 @@ func releaseAddColumnIsTagAndSyncTags(x *xorm.Engine) error {
pageSize := models.RepositoryListDefaultPageSize pageSize := models.RepositoryListDefaultPageSize
for { for {
repos := make([]*models.Repository, 0, pageSize) repos := make([]*models.Repository, 0, pageSize)
if err := x.Table("repository").Asc("id").Limit(pageSize, offset).Find(&repos); err != nil { if err := x.Table("repository").Cols("id", "name", "owner_id").Asc("id").Limit(pageSize, offset).Find(&repos); err != nil {
return fmt.Errorf("select repos [offset: %d]: %v", offset, err) return fmt.Errorf("select repos [offset: %d]: %v", offset, err)
} }
for _, repo := range repos { for _, repo := range repos {

View File

@ -120,6 +120,14 @@ func addMultipleAssignees(x *xorm.Engine) error {
} }
} }
// Commit and begin new transaction for dropping columns
if err := sess.Commit(); err != nil {
return err
}
if err := sess.Begin(); err != nil {
return err
}
if err := dropTableColumns(sess, "issue", "assignee_id"); err != nil { if err := dropTableColumns(sess, "issue", "assignee_id"); err != nil {
return err return err
} }

View File

@ -5,13 +5,18 @@
package migrations package migrations
import ( import (
<<<<<<< HEAD
"fmt" "fmt"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
=======
"code.gitea.io/gitea/modules/setting"
>>>>>>> origin/master
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
) )
<<<<<<< HEAD
func addPullRequestRebaseWithMergeCommit(x *xorm.Engine) error { func addPullRequestRebaseWithMergeCommit(x *xorm.Engine) error {
// RepoUnit describes all units of a repository // RepoUnit describes all units of a repository
type RepoUnit struct { type RepoUnit struct {
@ -20,6 +25,60 @@ func addPullRequestRebaseWithMergeCommit(x *xorm.Engine) error {
Type int `xorm:"INDEX(s)"` Type int `xorm:"INDEX(s)"`
Config map[string]interface{} `xorm:"JSON"` Config map[string]interface{} `xorm:"JSON"`
CreatedUnix util.TimeStamp `xorm:"INDEX CREATED"` CreatedUnix util.TimeStamp `xorm:"INDEX CREATED"`
=======
func removeStaleWatches(x *xorm.Engine) error {
type Watch struct {
ID int64
UserID int64
RepoID int64
}
type IssueWatch struct {
ID int64
UserID int64
RepoID int64
IsWatching bool
}
type Repository struct {
ID int64
IsPrivate bool
OwnerID int64
}
type Access struct {
UserID int64
RepoID int64
Mode int
}
const (
// AccessModeNone no access
AccessModeNone int = iota // 0
// AccessModeRead read access
AccessModeRead // 1
)
accessLevel := func(userID int64, repo *Repository) (int, error) {
mode := AccessModeNone
if !repo.IsPrivate {
mode = AccessModeRead
}
if userID == 0 {
return mode, nil
}
if userID == repo.OwnerID {
return 4, nil
}
a := &Access{UserID: userID, RepoID: repo.ID}
if has, err := x.Get(a); !has || err != nil {
return mode, err
}
return a.Mode, nil
>>>>>>> origin/master
} }
sess := x.NewSession() sess := x.NewSession()
@ -28,6 +87,7 @@ func addPullRequestRebaseWithMergeCommit(x *xorm.Engine) error {
return err return err
} }
<<<<<<< HEAD
//Updating existing issue units //Updating existing issue units
units := make([]*RepoUnit, 0, 100) units := make([]*RepoUnit, 0, 100)
if err := sess.Where("`type` = ?", V16UnitTypePRs).Find(&units); err != nil { if err := sess.Where("`type` = ?", V16UnitTypePRs).Find(&units); err != nil {
@ -46,4 +106,91 @@ func addPullRequestRebaseWithMergeCommit(x *xorm.Engine) error {
} }
return sess.Commit() return sess.Commit()
=======
repoCache := make(map[int64]*Repository)
err := x.BufferSize(setting.IterateBufferSize).Iterate(new(Watch),
func(idx int, bean interface{}) error {
watch := bean.(*Watch)
repo := repoCache[watch.RepoID]
if repo == nil {
repo = &Repository{
ID: watch.RepoID,
}
if _, err := x.Get(repo); err != nil {
return err
}
repoCache[watch.RepoID] = repo
}
// Remove watches from now unaccessible repositories
mode, err := accessLevel(watch.UserID, repo)
if err != nil {
return err
}
has := AccessModeRead <= mode
if has {
return nil
}
if _, err = sess.Delete(&Watch{0, watch.UserID, repo.ID}); err != nil {
return err
}
_, err = sess.Exec("UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?", repo.ID)
return err
})
if err != nil {
return err
}
repoCache = make(map[int64]*Repository)
err = x.BufferSize(setting.IterateBufferSize).
Distinct("issue_watch.user_id", "issue.repo_id").
Join("INNER", "issue", "issue_watch.issue_id = issue.id").
Where("issue_watch.is_watching = ?", true).
Iterate(new(IssueWatch),
func(idx int, bean interface{}) error {
watch := bean.(*IssueWatch)
repo := repoCache[watch.RepoID]
if repo == nil {
repo = &Repository{
ID: watch.RepoID,
}
if _, err := x.Get(repo); err != nil {
return err
}
repoCache[watch.RepoID] = repo
}
// Remove issue watches from now unaccssible repositories
mode, err := accessLevel(watch.UserID, repo)
if err != nil {
return err
}
has := AccessModeRead <= mode
if has {
return nil
}
iw := &IssueWatch{
IsWatching: false,
}
_, err = sess.
Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", watch.RepoID).
Cols("is_watching", "updated_unix").
Where("`issue_watch`.user_id = ?", watch.UserID).
Update(iw)
return err
})
if err != nil {
return err
}
return sess.Commit()
>>>>>>> origin/master
} }

213
models/migrations/v68.go Normal file
View File

@ -0,0 +1,213 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"regexp"
"strings"
"code.gitea.io/gitea/modules/log"
"github.com/go-xorm/xorm"
)
var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
func validateTopic(topic string) bool {
return len(topic) <= 35 && topicPattern.MatchString(topic)
}
func reformatAndRemoveIncorrectTopics(x *xorm.Engine) (err error) {
log.Info("This migration could take up to minutes, please be patient.")
type Topic struct {
ID int64
Name string `xorm:"UNIQUE"`
RepoCount int
CreatedUnix int64 `xorm:"INDEX created"`
UpdatedUnix int64 `xorm:"INDEX updated"`
}
type RepoTopic struct {
RepoID int64 `xorm:"UNIQUE(s)"`
TopicID int64 `xorm:"UNIQUE(s)"`
}
type Repository struct {
ID int64 `xorm:"pk autoincr"`
Topics []string `xorm:"TEXT JSON"`
}
if err := x.Sync2(new(Topic)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
if err := x.Sync2(new(RepoTopic)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
sess := x.NewSession()
defer sess.Close()
const batchSize = 100
touchedRepo := make(map[int64]struct{})
delTopicIDs := make([]int64, 0, batchSize)
log.Info("Validating existed topics...")
if err := sess.Begin(); err != nil {
return err
}
for start := 0; ; start += batchSize {
topics := make([]*Topic, 0, batchSize)
if err := x.Cols("id", "name").Asc("id").Limit(batchSize, start).Find(&topics); err != nil {
return err
}
if len(topics) == 0 {
break
}
for _, topic := range topics {
if validateTopic(topic.Name) {
continue
}
log.Info("Incorrect topic: id = %v, name = %q", topic.ID, topic.Name)
topic.Name = strings.Replace(strings.TrimSpace(strings.ToLower(topic.Name)), " ", "-", -1)
ids := make([]int64, 0, 30)
if err := sess.Table("repo_topic").Cols("repo_id").
Where("topic_id = ?", topic.ID).Find(&ids); err != nil {
return err
}
log.Info("Touched repo ids: %v", ids)
for _, id := range ids {
touchedRepo[id] = struct{}{}
}
if validateTopic(topic.Name) {
unifiedTopic := Topic{Name: topic.Name}
exists, err := sess.Cols("id", "name").Get(&unifiedTopic)
log.Info("Exists topic with the name %q? %v, id = %v", topic.Name, exists, unifiedTopic.ID)
if err != nil {
return err
}
if exists {
log.Info("Updating repo_topic rows with topic_id = %v to topic_id = %v", topic.ID, unifiedTopic.ID)
if _, err := sess.Where("topic_id = ? AND repo_id NOT IN "+
"(SELECT rt1.repo_id FROM repo_topic rt1 INNER JOIN repo_topic rt2 "+
"ON rt1.repo_id = rt2.repo_id WHERE rt1.topic_id = ? AND rt2.topic_id = ?)",
topic.ID, topic.ID, unifiedTopic.ID).Update(&RepoTopic{TopicID: unifiedTopic.ID}); err != nil {
return err
}
log.Info("Updating topic `repo_count` field")
if _, err := sess.Exec(
"UPDATE topic SET repo_count = (SELECT COUNT(*) FROM repo_topic WHERE topic_id = ? GROUP BY topic_id) WHERE id = ?",
unifiedTopic.ID, unifiedTopic.ID); err != nil {
return err
}
} else {
log.Info("Updating topic: id = %v, name = %q", topic.ID, topic.Name)
if _, err := sess.Table("topic").ID(topic.ID).
Update(&Topic{Name: topic.Name}); err != nil {
return err
}
continue
}
}
delTopicIDs = append(delTopicIDs, topic.ID)
}
}
if err := sess.Commit(); err != nil {
return err
}
sess.Init()
log.Info("Deleting incorrect topics...")
if err := sess.Begin(); err != nil {
return err
}
log.Info("Deleting 'repo_topic' rows for topics with ids = %v", delTopicIDs)
if _, err := sess.In("topic_id", delTopicIDs).Delete(&RepoTopic{}); err != nil {
return err
}
log.Info("Deleting topics with id = %v", delTopicIDs)
if _, err := sess.In("id", delTopicIDs).Delete(&Topic{}); err != nil {
return err
}
if err := sess.Commit(); err != nil {
return err
}
delRepoTopics := make([]*RepoTopic, 0, batchSize)
log.Info("Checking the number of topics in the repositories...")
for start := 0; ; start += batchSize {
repoTopics := make([]*RepoTopic, 0, batchSize)
if err := x.Cols("repo_id").Asc("repo_id").Limit(batchSize, start).
GroupBy("repo_id").Having("COUNT(*) > 25").Find(&repoTopics); err != nil {
return err
}
if len(repoTopics) == 0 {
break
}
log.Info("Number of repositories with more than 25 topics: %v", len(repoTopics))
for _, repoTopic := range repoTopics {
touchedRepo[repoTopic.RepoID] = struct{}{}
tmpRepoTopics := make([]*RepoTopic, 0, 30)
if err := x.Where("repo_id = ?", repoTopic.RepoID).Find(&tmpRepoTopics); err != nil {
return err
}
log.Info("Repository with id = %v has %v topics", repoTopic.RepoID, len(tmpRepoTopics))
for i := len(tmpRepoTopics) - 1; i > 24; i-- {
delRepoTopics = append(delRepoTopics, tmpRepoTopics[i])
}
}
}
sess.Init()
log.Info("Deleting superfluous topics for repositories (more than 25 topics)...")
if err := sess.Begin(); err != nil {
return err
}
for _, repoTopic := range delRepoTopics {
log.Info("Deleting 'repo_topic' rows for 'repository' with id = %v. Topic id = %v",
repoTopic.RepoID, repoTopic.TopicID)
if _, err := sess.Where("repo_id = ? AND topic_id = ?", repoTopic.RepoID,
repoTopic.TopicID).Delete(&RepoTopic{}); err != nil {
return err
}
if _, err := sess.Exec(
"UPDATE topic SET repo_count = (SELECT repo_count FROM topic WHERE id = ?) - 1 WHERE id = ?",
repoTopic.TopicID, repoTopic.TopicID); err != nil {
return err
}
}
log.Info("Updating repositories 'topics' fields...")
for repoID := range touchedRepo {
topicNames := make([]string, 0, 30)
if err := sess.Table("topic").Cols("name").
Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil {
return err
}
log.Info("Updating 'topics' field for repository with id = %v", repoID)
if _, err := sess.ID(repoID).Cols("topics").
Update(&Repository{Topics: topicNames}); err != nil {
return err
}
}
if err := sess.Commit(); err != nil {
return err
}
return nil
}

88
models/migrations/v69.go Normal file
View File

@ -0,0 +1,88 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"github.com/go-xorm/xorm"
)
func moveTeamUnitsToTeamUnitTable(x *xorm.Engine) error {
// Team see models/team.go
type Team struct {
ID int64
OrgID int64
UnitTypes []int `xorm:"json"`
}
// TeamUnit see models/org_team.go
type TeamUnit struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
TeamID int64 `xorm:"UNIQUE(s)"`
Type int `xorm:"UNIQUE(s)"`
}
if err := x.Sync2(new(TeamUnit)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
// Update team unit types
const batchSize = 100
for start := 0; ; start += batchSize {
teams := make([]*Team, 0, batchSize)
if err := x.Limit(batchSize, start).Find(&teams); err != nil {
return err
}
if len(teams) == 0 {
break
}
for _, team := range teams {
var unitTypes []int
if len(team.UnitTypes) == 0 {
unitTypes = allUnitTypes
} else {
unitTypes = team.UnitTypes
}
// insert units for team
var units = make([]TeamUnit, 0, len(unitTypes))
for _, tp := range unitTypes {
units = append(units, TeamUnit{
OrgID: team.OrgID,
TeamID: team.ID,
Type: tp,
})
}
if _, err := sess.Insert(&units); err != nil {
return fmt.Errorf("Insert team units: %v", err)
}
}
}
// Commit and begin new transaction for dropping columns
if err := sess.Commit(); err != nil {
return err
}
if err := sess.Begin(); err != nil {
return err
}
if err := dropTableColumns(sess, "team", "unit_types"); err != nil {
return err
}
return sess.Commit()
}

100
models/migrations/v70.go Normal file
View File

@ -0,0 +1,100 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"time"
"code.gitea.io/gitea/modules/setting"
"github.com/go-xorm/xorm"
)
func addIssueDependencies(x *xorm.Engine) (err error) {
type IssueDependency struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
IssueID int64 `xorm:"NOT NULL"`
DependencyID int64 `xorm:"NOT NULL"`
Created time.Time `xorm:"-"`
CreatedUnix int64 `xorm:"created"`
Updated time.Time `xorm:"-"`
UpdatedUnix int64 `xorm:"updated"`
}
if err = x.Sync(new(IssueDependency)); err != nil {
return fmt.Errorf("Error creating issue_dependency_table column definition: %v", err)
}
// Update Comment definition
// This (copied) struct does only contain fields used by xorm as the only use here is to update the database
// CommentType defines the comment type
type CommentType int
// TimeStamp defines a timestamp
type TimeStamp int64
type Comment struct {
ID int64 `xorm:"pk autoincr"`
Type CommentType
PosterID int64 `xorm:"INDEX"`
IssueID int64 `xorm:"INDEX"`
LabelID int64
OldMilestoneID int64
MilestoneID int64
OldAssigneeID int64
AssigneeID int64
OldTitle string
NewTitle string
DependentIssueID int64
CommitID int64
Line int64
Content string `xorm:"TEXT"`
CreatedUnix TimeStamp `xorm:"INDEX created"`
UpdatedUnix TimeStamp `xorm:"INDEX updated"`
// Reference issue in commit message
CommitSHA string `xorm:"VARCHAR(40)"`
}
if err = x.Sync(new(Comment)); err != nil {
return fmt.Errorf("Error updating issue_comment table column definition: %v", err)
}
// RepoUnit describes all units of a repository
type RepoUnit struct {
ID int64
RepoID int64 `xorm:"INDEX(s)"`
Type int `xorm:"INDEX(s)"`
Config map[string]interface{} `xorm:"JSON"`
CreatedUnix int64 `xorm:"INDEX CREATED"`
Created time.Time `xorm:"-"`
}
//Updating existing issue units
units := make([]*RepoUnit, 0, 100)
err = x.Where("`type` = ?", V16UnitTypeIssues).Find(&units)
if err != nil {
return fmt.Errorf("Query repo units: %v", err)
}
for _, unit := range units {
if unit.Config == nil {
unit.Config = make(map[string]interface{})
}
if _, ok := unit.Config["EnableDependencies"]; !ok {
unit.Config["EnableDependencies"] = setting.Service.DefaultEnableDependencies
}
if _, err := x.ID(unit.ID).Cols("config").Update(unit); err != nil {
return err
}
}
return err
}

48
models/migrations/v71.go Normal file
View File

@ -0,0 +1,48 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"code.gitea.io/gitea/modules/util"
"github.com/go-xorm/xorm"
)
func addPullRequestRebaseWithMergeCommit(x *xorm.Engine) error {
// RepoUnit describes all units of a repository
type RepoUnit struct {
ID int64
RepoID int64 `xorm:"INDEX(s)"`
Type int `xorm:"INDEX(s)"`
Config map[string]interface{} `xorm:"JSON"`
CreatedUnix util.TimeStamp `xorm:"INDEX CREATED"`
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
//Updating existing issue units
units := make([]*RepoUnit, 0, 100)
if err := sess.Where("`type` = ?", V16UnitTypePRs).Find(&units); err != nil {
return fmt.Errorf("Query repo units: %v", err)
}
for _, unit := range units {
if unit.Config == nil {
unit.Config = make(map[string]interface{})
}
if _, ok := unit.Config["AllowRebaseMergeCommit"]; !ok {
unit.Config["AllowRebaseMergeCommit"] = true
}
if _, err := sess.ID(unit.ID).Cols("config").Update(unit); err != nil {
return err
}
}
return sess.Commit()
}

View File

@ -1,4 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved. // Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -117,10 +118,12 @@ func init() {
new(TrackedTime), new(TrackedTime),
new(DeletedBranch), new(DeletedBranch),
new(RepoIndexerStatus), new(RepoIndexerStatus),
new(IssueDependency),
new(LFSLock), new(LFSLock),
new(Reaction), new(Reaction),
new(IssueAssignees), new(IssueAssignees),
new(U2FRegistration), new(U2FRegistration),
new(TeamUnit),
) )
gonicNames := []string{"SSL", "UID"} gonicNames := []string{"SSL", "UID"}
@ -184,6 +187,18 @@ func parsePostgreSQLHostPort(info string) (string, string) {
return host, port return host, port
} }
func getPostgreSQLConnectionString(DBHost, DBUser, DBPasswd, DBName, DBParam, DBSSLMode string) (connStr string) {
host, port := parsePostgreSQLHostPort(DBHost)
if host[0] == '/' { // looks like a unix socket
connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s",
url.PathEscape(DBUser), url.PathEscape(DBPasswd), port, DBName, DBParam, DBSSLMode, host)
} else {
connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s",
url.PathEscape(DBUser), url.PathEscape(DBPasswd), host, port, DBName, DBParam, DBSSLMode)
}
return
}
func parseMSSQLHostPort(info string) (string, string) { func parseMSSQLHostPort(info string) (string, string) {
host, port := "127.0.0.1", "1433" host, port := "127.0.0.1", "1433"
if strings.Contains(info, ":") { if strings.Contains(info, ":") {
@ -214,14 +229,7 @@ func getEngine() (*xorm.Engine, error) {
DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name, Param) DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name, Param)
} }
case "postgres": case "postgres":
host, port := parsePostgreSQLHostPort(DbCfg.Host) connStr = getPostgreSQLConnectionString(DbCfg.Host, DbCfg.User, DbCfg.Passwd, DbCfg.Name, Param, DbCfg.SSLMode)
if host[0] == '/' { // looks like a unix socket
connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s",
url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Passwd), port, DbCfg.Name, Param, DbCfg.SSLMode, host)
} else {
connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s",
url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Passwd), host, port, DbCfg.Name, Param, DbCfg.SSLMode)
}
case "mssql": case "mssql":
host, port := parseMSSQLHostPort(DbCfg.Host) host, port := parseMSSQLHostPort(DbCfg.Host)
connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, DbCfg.Name, DbCfg.User, DbCfg.Passwd) connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, DbCfg.Name, DbCfg.User, DbCfg.Passwd)

View File

@ -1,4 +1,5 @@
// Copyright 2016 The Gogs Authors. All rights reserved. // Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -53,3 +54,42 @@ func Test_parsePostgreSQLHostPort(t *testing.T) {
assert.Equal(t, test.Port, port) assert.Equal(t, test.Port, port)
} }
} }
func Test_getPostgreSQLConnectionString(t *testing.T) {
tests := []struct {
Host string
Port string
User string
Passwd string
Name string
Param string
SSLMode string
Output string
}{
{
Host: "/tmp/pg.sock",
Port: "4321",
User: "testuser",
Passwd: "space space !#$%^^%^```-=?=",
Name: "gitea",
Param: "",
SSLMode: "false",
Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/giteasslmode=false&host=/tmp/pg.sock",
},
{
Host: "localhost",
Port: "1234",
User: "pgsqlusername",
Passwd: "I love Gitea!",
Name: "gitea",
Param: "",
SSLMode: "true",
Output: "postgres://pgsqlusername:I%20love%20Gitea%21@localhost:5432/giteasslmode=true",
},
}
for _, test := range tests {
connStr := getPostgreSQLConnectionString(test.Host, test.User, test.Passwd, test.Name, test.Param, test.SSLMode)
assert.Equal(t, test.Output, connStr)
}
}

View File

@ -1,18 +0,0 @@
// +build tidb
// Copyright 2015 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
_ "github.com/go-xorm/tidb"
"github.com/ngaut/log"
_ "github.com/pingcap/tidb"
)
func init() {
EnableTiDB = true
log.SetLevelByString("error")
}

View File

@ -119,7 +119,17 @@ func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthor
} }
} }
issue.loadRepo(e)
for _, watch := range watches { for _, watch := range watches {
issue.Repo.Units = nil
if issue.IsPull && !issue.Repo.CheckUnitUser(watch.UserID, false, UnitTypePullRequests) {
continue
}
if !issue.IsPull && !issue.Repo.CheckUnitUser(watch.UserID, false, UnitTypeIssues) {
continue
}
if err := notifyUser(watch.UserID); err != nil { if err := notifyUser(watch.UserID); err != nil {
return err return err
} }

View File

@ -154,12 +154,26 @@ func CreateOrganization(org, owner *User) (err error) {
Name: ownerTeamName, Name: ownerTeamName,
Authorize: AccessModeOwner, Authorize: AccessModeOwner,
NumMembers: 1, NumMembers: 1,
UnitTypes: allRepUnitTypes,
} }
if _, err = sess.Insert(t); err != nil { if _, err = sess.Insert(t); err != nil {
return fmt.Errorf("insert owner team: %v", err) return fmt.Errorf("insert owner team: %v", err)
} }
// insert units for team
var units = make([]TeamUnit, 0, len(allRepUnitTypes))
for _, tp := range allRepUnitTypes {
units = append(units, TeamUnit{
OrgID: org.ID,
TeamID: t.ID,
Type: tp,
})
}
if _, err = sess.Insert(&units); err != nil {
sess.Rollback()
return err
}
if _, err = sess.Insert(&TeamUser{ if _, err = sess.Insert(&TeamUser{
UID: owner.ID, UID: owner.ID,
OrgID: org.ID, OrgID: org.ID,
@ -238,6 +252,7 @@ func deleteOrg(e *xorm.Session, u *User) error {
&Team{OrgID: u.ID}, &Team{OrgID: u.ID},
&OrgUser{OrgID: u.ID}, &OrgUser{OrgID: u.ID},
&TeamUser{OrgID: u.ID}, &TeamUser{OrgID: u.ID},
&TeamUnit{OrgID: u.ID},
); err != nil { ); err != nil {
return fmt.Errorf("deleteBeans: %v", err) return fmt.Errorf("deleteBeans: %v", err)
} }
@ -368,7 +383,7 @@ func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
func GetOrgUsersByUserID(uid int64, all bool) ([]*OrgUser, error) { func GetOrgUsersByUserID(uid int64, all bool) ([]*OrgUser, error) {
ous := make([]*OrgUser, 0, 10) ous := make([]*OrgUser, 0, 10)
sess := x. sess := x.
Join("LEFT", "user", "`org_user`.org_id=`user`.id"). Join("LEFT", "`user`", "`org_user`.org_id=`user`.id").
Where("`org_user`.uid=?", uid) Where("`org_user`.uid=?", uid)
if !all { if !all {
// Only show public organizations // Only show public organizations
@ -560,7 +575,7 @@ func (org *User) getUserTeams(e Engine, userID int64, cols ...string) ([]*Team,
return teams, e. return teams, e.
Where("`team_user`.org_id = ?", org.ID). Where("`team_user`.org_id = ?", org.ID).
Join("INNER", "team_user", "`team_user`.team_id = team.id"). Join("INNER", "team_user", "`team_user`.team_id = team.id").
Join("INNER", "user", "`user`.id=team_user.uid"). Join("INNER", "`user`", "`user`.id=team_user.uid").
And("`team_user`.uid = ?", userID). And("`team_user`.uid = ?", userID).
Asc("`user`.name"). Asc("`user`.name").
Cols(cols...). Cols(cols...).

View File

@ -1,3 +1,4 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Copyright 2016 The Gogs Authors. All rights reserved. // Copyright 2016 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -10,7 +11,6 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
) )
@ -28,15 +28,16 @@ type Team struct {
Members []*User `xorm:"-"` Members []*User `xorm:"-"`
NumRepos int NumRepos int
NumMembers int NumMembers int
UnitTypes []UnitType `xorm:"json"` Units []*TeamUnit `xorm:"-"`
} }
// GetUnitTypes returns unit types the team owned, empty means all the unit types func (t *Team) getUnits(e Engine) (err error) {
func (t *Team) GetUnitTypes() []UnitType { if t.Units != nil {
if len(t.UnitTypes) == 0 { return nil
return allRepUnitTypes
} }
return t.UnitTypes
t.Units, err = getUnitsByTeamID(e, t.ID)
return err
} }
// HasWriteAccess returns true if team has at least write level access mode. // HasWriteAccess returns true if team has at least write level access mode.
@ -178,6 +179,11 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e
if err = watchRepo(e, teamUser.UID, repo.ID, false); err != nil { if err = watchRepo(e, teamUser.UID, repo.ID, false); err != nil {
return err return err
} }
// Remove all IssueWatches a user has subscribed to in the repositories
if err := removeIssueWatchersByRepoID(e, teamUser.UID, repo.ID); err != nil {
return err
}
} }
return nil return nil
@ -209,11 +215,12 @@ func (t *Team) RemoveRepository(repoID int64) error {
// UnitEnabled returns if the team has the given unit type enabled // UnitEnabled returns if the team has the given unit type enabled
func (t *Team) UnitEnabled(tp UnitType) bool { func (t *Team) UnitEnabled(tp UnitType) bool {
if len(t.UnitTypes) == 0 { if err := t.getUnits(x); err != nil {
return true log.Warn("Error loading repository (ID: %d) units: %s", t.ID, err.Error())
} }
for _, u := range t.UnitTypes {
if u == tp { for _, unit := range t.Units {
if unit.Type == tp {
return true return true
} }
} }
@ -270,6 +277,17 @@ func NewTeam(t *Team) (err error) {
return err return err
} }
// insert units for team
if len(t.Units) > 0 {
for _, unit := range t.Units {
unit.TeamID = t.ID
}
if _, err = sess.Insert(&t.Units); err != nil {
sess.Rollback()
return err
}
}
// Update organization number of teams. // Update organization number of teams.
if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil { if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil {
sess.Rollback() sess.Rollback()
@ -374,11 +392,34 @@ func DeleteTeam(t *Team) error {
return err return err
} }
if err := t.getMembers(sess); err != nil {
return err
}
// Delete all accesses. // Delete all accesses.
for _, repo := range t.Repos { for _, repo := range t.Repos {
if err := repo.recalculateTeamAccesses(sess, t.ID); err != nil { if err := repo.recalculateTeamAccesses(sess, t.ID); err != nil {
return err return err
} }
// Remove watches from all users and now unaccessible repos
for _, user := range t.Members {
has, err := hasAccess(sess, user.ID, repo, AccessModeRead)
if err != nil {
return err
} else if has {
continue
}
if err = watchRepo(sess, user.ID, repo.ID, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repositories
if err = removeIssueWatchersByRepoID(sess, user.ID, repo.ID); err != nil {
return err
}
}
} }
// Delete team-repo // Delete team-repo
@ -396,6 +437,13 @@ func DeleteTeam(t *Team) error {
return err return err
} }
// Delete team-unit.
if _, err := sess.
Where("team_id=?", t.ID).
Delete(new(TeamUnit)); err != nil {
return err
}
// Delete team. // Delete team.
if _, err := sess.ID(t.ID).Delete(new(Team)); err != nil { if _, err := sess.ID(t.ID).Delete(new(Team)); err != nil {
return err return err
@ -518,6 +566,10 @@ func AddTeamMember(team *Team, userID int64) error {
if err := repo.recalculateTeamAccesses(sess, 0); err != nil { if err := repo.recalculateTeamAccesses(sess, 0); err != nil {
return err return err
} }
if err = watchRepo(sess, userID, repo.ID, true); err != nil {
return err
}
} }
return sess.Commit() return sess.Commit()
@ -558,6 +610,23 @@ func removeTeamMember(e *xorm.Session, team *Team, userID int64) error {
if err := repo.recalculateTeamAccesses(e, 0); err != nil { if err := repo.recalculateTeamAccesses(e, 0); err != nil {
return err return err
} }
// Remove watches from now unaccessible
has, err := hasAccess(e, userID, repo, AccessModeRead)
if err != nil {
return err
} else if has {
continue
}
if err = watchRepo(e, userID, repo.ID, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repositories
if err := removeIssueWatchersByRepoID(e, userID, repo.ID); err != nil {
return err
}
} }
// Check if the user is a member of any team in the organization. // Check if the user is a member of any team in the organization.
@ -646,3 +715,47 @@ func GetTeamsWithAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, er
And("team_repo.repo_id = ?", repoID). And("team_repo.repo_id = ?", repoID).
Find(&teams) Find(&teams)
} }
// ___________ ____ ___ .__ __
// \__ ___/___ _____ _____ | | \____ |__|/ |_
// | |_/ __ \\__ \ / \| | / \| \ __\
// | |\ ___/ / __ \| Y Y \ | / | \ || |
// |____| \___ >____ /__|_| /______/|___| /__||__|
// \/ \/ \/ \/
// TeamUnit describes all units of a repository
type TeamUnit struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
TeamID int64 `xorm:"UNIQUE(s)"`
Type UnitType `xorm:"UNIQUE(s)"`
}
// Unit returns Unit
func (t *TeamUnit) Unit() Unit {
return Units[t.Type]
}
func getUnitsByTeamID(e Engine, teamID int64) (units []*TeamUnit, err error) {
return units, e.Where("team_id = ?", teamID).Find(&units)
}
// UpdateTeamUnits updates a teams's units
func UpdateTeamUnits(team *Team, units []TeamUnit) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if _, err = sess.Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil {
return err
}
if _, err = sess.Insert(units); err != nil {
sess.Rollback()
return err
}
return sess.Commit()
}

View File

@ -489,8 +489,8 @@ func TestAccessibleReposEnv_CountRepos(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, expectedCount, count) assert.EqualValues(t, expectedCount, count)
} }
testSuccess(2, 2) testSuccess(2, 3)
testSuccess(4, 1) testSuccess(4, 2)
} }
func TestAccessibleReposEnv_RepoIDs(t *testing.T) { func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
@ -503,8 +503,8 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expectedRepoIDs, repoIDs) assert.Equal(t, expectedRepoIDs, repoIDs)
} }
testSuccess(2, 1, 100, []int64{3, 5}) testSuccess(2, 1, 100, []int64{3, 5, 32})
testSuccess(4, 0, 100, []int64{3}) testSuccess(4, 0, 100, []int64{3, 32})
} }
func TestAccessibleReposEnv_Repos(t *testing.T) { func TestAccessibleReposEnv_Repos(t *testing.T) {
@ -522,8 +522,8 @@ func TestAccessibleReposEnv_Repos(t *testing.T) {
} }
assert.Equal(t, expectedRepos, repos) assert.Equal(t, expectedRepos, repos)
} }
testSuccess(2, []int64{3, 5}) testSuccess(2, []int64{3, 5, 32})
testSuccess(4, []int64{3}) testSuccess(4, []int64{3, 32})
} }
func TestAccessibleReposEnv_MirrorRepos(t *testing.T) { func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {

View File

@ -448,6 +448,11 @@ func DeleteReleaseByID(id int64, u *User, delTag bool) error {
} }
} }
rel.Repo = repo
if err = rel.LoadAttributes(); err != nil {
return fmt.Errorf("LoadAttributes: %v", err)
}
mode, _ := accessLevel(x, u.ID, rel.Repo) mode, _ := accessLevel(x, u.ID, rel.Repo)
if err := PrepareWebhooks(rel.Repo, HookEventRelease, &api.ReleasePayload{ if err := PrepareWebhooks(rel.Repo, HookEventRelease, &api.ReleasePayload{
Action: api.HookReleaseDeleted, Action: api.HookReleaseDeleted,

View File

@ -365,22 +365,14 @@ func (repo *Repository) getUnitsByUserID(e Engine, userID int64, isAdmin bool) (
return err return err
} }
var allTypes = make(map[UnitType]struct{}, len(allRepUnitTypes))
for _, team := range teams {
// Administrators can not be limited
if team.Authorize >= AccessModeAdmin {
return nil
}
for _, unitType := range team.UnitTypes {
allTypes[unitType] = struct{}{}
}
}
// unique // unique
var newRepoUnits = make([]*RepoUnit, 0, len(repo.Units)) var newRepoUnits = make([]*RepoUnit, 0, len(repo.Units))
for _, u := range repo.Units { for _, u := range repo.Units {
if _, ok := allTypes[u.Type]; ok { for _, team := range teams {
newRepoUnits = append(newRepoUnits, u) if team.UnitEnabled(u.Type) {
newRepoUnits = append(newRepoUnits, u)
break
}
} }
} }
@ -789,7 +781,7 @@ var (
// DescriptionHTML does special handles to description and return HTML string. // DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML() template.HTML { func (repo *Repository) DescriptionHTML() template.HTML {
sanitize := func(s string) string { sanitize := func(s string) string {
return fmt.Sprintf(`<a href="%[1]s" target="_blank" rel="noopener">%[1]s</a>`, s) return fmt.Sprintf(`<a href="%[1]s" target="_blank" rel="noopener noreferrer">%[1]s</a>`, s)
} }
return template.HTML(descPattern.ReplaceAllStringFunc(markup.Sanitize(repo.Description), sanitize)) return template.HTML(descPattern.ReplaceAllStringFunc(markup.Sanitize(repo.Description), sanitize))
} }
@ -1353,7 +1345,17 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err
units = append(units, RepoUnit{ units = append(units, RepoUnit{
RepoID: repo.ID, RepoID: repo.ID,
Type: tp, Type: tp,
Config: &IssuesConfig{EnableTimetracker: setting.Service.DefaultEnableTimetracking, AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime}, Config: &IssuesConfig{
EnableTimetracker: setting.Service.DefaultEnableTimetracking,
AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime,
EnableDependencies: setting.Service.DefaultEnableDependencies,
},
})
} else if tp == UnitTypePullRequests {
units = append(units, RepoUnit{
RepoID: repo.ID,
Type: tp,
Config: &PullRequestsConfig{AllowMerge: true, AllowRebase: true, AllowSquash: true},
}) })
} else { } else {
units = append(units, RepoUnit{ units = append(units, RepoUnit{
@ -1848,6 +1850,12 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
if _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{}); err != nil { if _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{}); err != nil {
return err return err
} }
if _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{}); err != nil {
return err
}
if _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{}); err != nil {
return err
}
attachments := make([]*Attachment, 0, 5) attachments := make([]*Attachment, 0, 5)
if err = sess. if err = sess.
@ -1950,7 +1958,7 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
func GetRepositoryByOwnerAndName(ownerName, repoName string) (*Repository, error) { func GetRepositoryByOwnerAndName(ownerName, repoName string) (*Repository, error) {
var repo Repository var repo Repository
has, err := x.Select("repository.*"). has, err := x.Select("repository.*").
Join("INNER", "user", "`user`.id = repository.owner_id"). Join("INNER", "`user`", "`user`.id = repository.owner_id").
Where("repository.lower_name = ?", strings.ToLower(repoName)). Where("repository.lower_name = ?", strings.ToLower(repoName)).
And("`user`.lower_name = ?", strings.ToLower(ownerName)). And("`user`.lower_name = ?", strings.ToLower(ownerName)).
Get(&repo) Get(&repo)

View File

@ -172,5 +172,14 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) {
return err return err
} }
if err = watchRepo(sess, uid, repo.ID, false); err != nil {
return err
}
// Remove all IssueWatches a user has subscribed to in the repository
if err := removeIssueWatchersByRepoID(sess, uid, repo.ID); err != nil {
return err
}
return sess.Commit() return sess.Commit()
} }

View File

@ -147,10 +147,10 @@ func TestSearchRepositoryByName(t *testing.T) {
count: 14}, count: 14},
{name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", {name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true}, opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true},
count: 15}, count: 16},
{name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true},
count: 19}, count: 20},
{name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true},
count: 13}, count: 13},
@ -159,7 +159,7 @@ func TestSearchRepositoryByName(t *testing.T) {
count: 11}, count: 11},
{name: "AllPublic/PublicRepositoriesOfOrganization", {name: "AllPublic/PublicRepositoriesOfOrganization",
opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse}, opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse},
count: 15}, count: 16},
} }
for _, testCase := range testCases { for _, testCase := range testCases {

View File

@ -73,6 +73,7 @@ func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
type IssuesConfig struct { type IssuesConfig struct {
EnableTimetracker bool EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool AllowOnlyContributorsToTrackTime bool
EnableDependencies bool
} }
// FromDB fills up a IssuesConfig from serialized format. // FromDB fills up a IssuesConfig from serialized format.
@ -167,7 +168,6 @@ func (r *RepoUnit) IssuesConfig() *IssuesConfig {
func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig { func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
return r.Config.(*ExternalTrackerConfig) return r.Config.(*ExternalTrackerConfig)
} }
func getUnitsByRepoID(e Engine, repoID int64) (units []*RepoUnit, err error) { func getUnitsByRepoID(e Engine, repoID int64) (units []*RepoUnit, err error) {
return units, e.Where("repo_id = ?", repoID).Find(&units) return units, e.Where("repo_id = ?", repoID).Find(&units)
} }

View File

@ -54,7 +54,7 @@ func getWatchers(e Engine, repoID int64) ([]*Watch, error) {
return watches, e.Where("`watch`.repo_id=?", repoID). return watches, e.Where("`watch`.repo_id=?", repoID).
And("`user`.is_active=?", true). And("`user`.is_active=?", true).
And("`user`.prohibit_login=?", false). And("`user`.prohibit_login=?", false).
Join("INNER", "user", "`user`.id = `watch`.user_id"). Join("INNER", "`user`", "`user`.id = `watch`.user_id").
Find(&watches) Find(&watches)
} }
@ -109,6 +109,23 @@ func notifyWatchers(e Engine, act *Action) error {
act.ID = 0 act.ID = 0
act.UserID = watches[i].UserID act.UserID = watches[i].UserID
act.Repo.Units = nil
switch act.OpType {
case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionDeleteBranch:
if !act.Repo.CheckUnitUser(act.UserID, false, UnitTypeCode) {
continue
}
case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue:
if !act.Repo.CheckUnitUser(act.UserID, false, UnitTypeIssues) {
continue
}
case ActionCreatePullRequest, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest:
if !act.Repo.CheckUnitUser(act.UserID, false, UnitTypePullRequests) {
continue
}
}
if _, err = e.InsertOne(act); err != nil { if _, err = e.InsertOne(act); err != nil {
return fmt.Errorf("insert new action: %v", err) return fmt.Errorf("insert new action: %v", err)
} }

View File

@ -6,6 +6,7 @@ package models
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -20,10 +21,12 @@ func init() {
) )
} }
var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
// Topic represents a topic of repositories // Topic represents a topic of repositories
type Topic struct { type Topic struct {
ID int64 ID int64
Name string `xorm:"unique"` Name string `xorm:"UNIQUE"`
RepoCount int RepoCount int
CreatedUnix util.TimeStamp `xorm:"INDEX created"` CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
@ -31,8 +34,8 @@ type Topic struct {
// RepoTopic represents associated repositories and topics // RepoTopic represents associated repositories and topics
type RepoTopic struct { type RepoTopic struct {
RepoID int64 `xorm:"unique(s)"` RepoID int64 `xorm:"UNIQUE(s)"`
TopicID int64 `xorm:"unique(s)"` TopicID int64 `xorm:"UNIQUE(s)"`
} }
// ErrTopicNotExist represents an error that a topic is not exist // ErrTopicNotExist represents an error that a topic is not exist
@ -51,6 +54,11 @@ func (err ErrTopicNotExist) Error() string {
return fmt.Sprintf("topic is not exist [name: %s]", err.Name) return fmt.Sprintf("topic is not exist [name: %s]", err.Name)
} }
// ValidateTopic checks topics by length and match pattern rules
func ValidateTopic(topic string) bool {
return len(topic) <= 35 && topicPattern.MatchString(topic)
}
// GetTopicByName retrieves topic by name // GetTopicByName retrieves topic by name
func GetTopicByName(name string) (*Topic, error) { func GetTopicByName(name string) (*Topic, error) {
var topic Topic var topic Topic
@ -182,6 +190,13 @@ func SaveTopics(repoID int64, topicNames ...string) error {
} }
} }
topicNames = make([]string, 0, 25)
if err := sess.Table("topic").Cols("name").
Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil {
return err
}
if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{ if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{
Topics: topicNames, Topics: topicNames,
}); err != nil { }); err != nil {

View File

@ -55,3 +55,16 @@ func TestAddTopic(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, 2, len(topics)) assert.EqualValues(t, 2, len(topics))
} }
func TestTopicValidator(t *testing.T) {
assert.True(t, ValidateTopic("12345"))
assert.True(t, ValidateTopic("2-test"))
assert.True(t, ValidateTopic("test-3"))
assert.True(t, ValidateTopic("first"))
assert.True(t, ValidateTopic("second-test-topic"))
assert.True(t, ValidateTopic("third-project-topic-with-max-length"))
assert.False(t, ValidateTopic("$fourth-test,topic"))
assert.False(t, ValidateTopic("-fifth-test-topic"))
assert.False(t, ValidateTopic("sixth-go-project-topic-with-excess-length"))
}

View File

@ -374,9 +374,9 @@ func (u *User) GetFollowers(page int) ([]*User, error) {
Limit(ItemsPerPage, (page-1)*ItemsPerPage). Limit(ItemsPerPage, (page-1)*ItemsPerPage).
Where("follow.follow_id=?", u.ID) Where("follow.follow_id=?", u.ID)
if setting.UsePostgreSQL { if setting.UsePostgreSQL {
sess = sess.Join("LEFT", "follow", `"user".id=follow.user_id`) sess = sess.Join("LEFT", "follow", "`user`.id=follow.user_id")
} else { } else {
sess = sess.Join("LEFT", "follow", "user.id=follow.user_id") sess = sess.Join("LEFT", "follow", "`user`.id=follow.user_id")
} }
return users, sess.Find(&users) return users, sess.Find(&users)
} }
@ -393,9 +393,9 @@ func (u *User) GetFollowing(page int) ([]*User, error) {
Limit(ItemsPerPage, (page-1)*ItemsPerPage). Limit(ItemsPerPage, (page-1)*ItemsPerPage).
Where("follow.user_id=?", u.ID) Where("follow.user_id=?", u.ID)
if setting.UsePostgreSQL { if setting.UsePostgreSQL {
sess = sess.Join("LEFT", "follow", `"user".id=follow.follow_id`) sess = sess.Join("LEFT", "follow", "`user`.id=follow.follow_id")
} else { } else {
sess = sess.Join("LEFT", "follow", "user.id=follow.follow_id") sess = sess.Join("LEFT", "follow", "`user`.id=follow.follow_id")
} }
return users, sess.Find(&users) return users, sess.Find(&users)
} }
@ -433,6 +433,17 @@ func (u *User) IsPasswordSet() bool {
// UploadAvatar saves custom avatar for user. // UploadAvatar saves custom avatar for user.
// FIXME: split uploads to different subdirs in case we have massive users. // FIXME: split uploads to different subdirs in case we have massive users.
func (u *User) UploadAvatar(data []byte) error { func (u *User) UploadAvatar(data []byte) error {
imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("DecodeConfig: %v", err)
}
if imgCfg.Width > setting.AvatarMaxWidth {
return fmt.Errorf("Image width is to large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth)
}
if imgCfg.Height > setting.AvatarMaxHeight {
return fmt.Errorf("Image height is to large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight)
}
img, _, err := image.Decode(bytes.NewReader(data)) img, _, err := image.Decode(bytes.NewReader(data))
if err != nil { if err != nil {
return fmt.Errorf("Decode: %v", err) return fmt.Errorf("Decode: %v", err)
@ -546,28 +557,46 @@ func (u *User) GetRepositories(page, pageSize int) (err error) {
return err return err
} }
// GetRepositoryIDs returns repositories IDs where user owned // GetRepositoryIDs returns repositories IDs where user owned and has unittypes
func (u *User) GetRepositoryIDs() ([]int64, error) { func (u *User) GetRepositoryIDs(units ...UnitType) ([]int64, error) {
var ids []int64 var ids []int64
return ids, x.Table("repository").Cols("id").Where("owner_id = ?", u.ID).Find(&ids)
sess := x.Table("repository").Cols("repository.id")
if len(units) > 0 {
sess = sess.Join("INNER", "repo_unit", "repository.id = repo_unit.repo_id")
sess = sess.In("repo_unit.type", units)
}
return ids, sess.Where("owner_id = ?", u.ID).Find(&ids)
} }
// GetOrgRepositoryIDs returns repositories IDs where user's team owned // GetOrgRepositoryIDs returns repositories IDs where user's team owned and has unittypes
func (u *User) GetOrgRepositoryIDs() ([]int64, error) { func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) {
var ids []int64 var ids []int64
return ids, x.Table("repository").
sess := x.Table("repository").
Cols("repository.id"). Cols("repository.id").
Join("INNER", "team_user", "repository.owner_id = team_user.org_id AND team_user.uid = ?", u.ID). Join("INNER", "team_user", "repository.owner_id = team_user.org_id").
Join("INNER", "team_repo", "repository.is_private != ? OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true)
if len(units) > 0 {
sess = sess.Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id")
sess = sess.In("team_unit.type", units)
}
return ids, sess.
Where("team_user.uid = ?", u.ID).
GroupBy("repository.id").Find(&ids) GroupBy("repository.id").Find(&ids)
} }
// GetAccessRepoIDs returns all repositories IDs where user's or user is a team member organizations // GetAccessRepoIDs returns all repositories IDs where user's or user is a team member organizations
func (u *User) GetAccessRepoIDs() ([]int64, error) { func (u *User) GetAccessRepoIDs(units ...UnitType) ([]int64, error) {
ids, err := u.GetRepositoryIDs() ids, err := u.GetRepositoryIDs(units...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ids2, err := u.GetOrgRepositoryIDs() ids2, err := u.GetOrgRepositoryIDs(units...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -927,7 +956,7 @@ func deleteUser(e *xorm.Session, u *User) error {
Where("watch.user_id = ?", u.ID).Find(&watchedRepoIDs); err != nil { Where("watch.user_id = ?", u.ID).Find(&watchedRepoIDs); err != nil {
return fmt.Errorf("get all watches: %v", err) return fmt.Errorf("get all watches: %v", err)
} }
if _, err = e.Decr("num_watches").In("id", watchedRepoIDs).Update(new(Repository)); err != nil { if _, err = e.Decr("num_watches").In("id", watchedRepoIDs).NoAutoTime().Update(new(Repository)); err != nil {
return fmt.Errorf("decrease repository num_watches: %v", err) return fmt.Errorf("decrease repository num_watches: %v", err)
} }
// ***** END: Watch ***** // ***** END: Watch *****
@ -937,7 +966,7 @@ func deleteUser(e *xorm.Session, u *User) error {
if err = e.Table("star").Cols("star.repo_id"). if err = e.Table("star").Cols("star.repo_id").
Where("star.uid = ?", u.ID).Find(&starredRepoIDs); err != nil { Where("star.uid = ?", u.ID).Find(&starredRepoIDs); err != nil {
return fmt.Errorf("get all stars: %v", err) return fmt.Errorf("get all stars: %v", err)
} else if _, err = e.Decr("num_stars").In("id", starredRepoIDs).Update(new(Repository)); err != nil { } else if _, err = e.Decr("num_stars").In("id", starredRepoIDs).NoAutoTime().Update(new(Repository)); err != nil {
return fmt.Errorf("decrease repository num_stars: %v", err) return fmt.Errorf("decrease repository num_stars: %v", err)
} }
// ***** END: Star ***** // ***** END: Star *****

View File

@ -159,3 +159,25 @@ func BenchmarkHashPassword(b *testing.B) {
u.HashPassword(pass) u.HashPassword(pass)
} }
} }
func TestGetOrgRepositoryIDs(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User)
user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User)
accessibleRepos, err := user2.GetOrgRepositoryIDs()
assert.NoError(t, err)
// User 2's team has access to private repos 3, 5, repo 32 is a public repo of the organization
assert.Equal(t, []int64{3, 5, 32}, accessibleRepos)
accessibleRepos, err = user4.GetOrgRepositoryIDs()
assert.NoError(t, err)
// User 4's team has access to private repo 3, repo 32 is a public repo of the organization
assert.Equal(t, []int64{3, 32}, accessibleRepos)
accessibleRepos, err = user5.GetOrgRepositoryIDs()
assert.NoError(t, err)
// User 5's team has no access to any repo
assert.Len(t, accessibleRepos, 0)
}

View File

@ -163,7 +163,7 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo
profileURL = customURLMapping.ProfileURL profileURL = customURLMapping.ProfileURL
} }
} }
provider = gitlab.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL) provider = gitlab.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, "read_user")
case "gplus": case "gplus":
provider = gplus.New(clientID, clientSecret, callbackURL, "email") provider = gplus.New(clientID, clientSecret, callbackURL, "email")
case "openidConnect": case "openidConnect":

View File

@ -114,6 +114,7 @@ type RepoSettingForm struct {
PullsAllowSquash bool PullsAllowSquash bool
EnableTimetracker bool EnableTimetracker bool
AllowOnlyContributorsToTrackTime bool AllowOnlyContributorsToTrackTime bool
EnableIssueDependencies bool
// Admin settings // Admin settings
EnableHealthCheck bool EnableHealthCheck bool
@ -372,7 +373,7 @@ func (f *MergePullRequestForm) Validate(ctx *macaron.Context, errs binding.Error
// NewReleaseForm form for creating release // NewReleaseForm form for creating release
type NewReleaseForm struct { type NewReleaseForm struct {
TagName string `binding:"Required"` TagName string `binding:"Required;GitRefName"`
Target string `form:"tag_target" binding:"Required"` Target string `form:"tag_target" binding:"Required"`
Title string `binding:"Required"` Title string `binding:"Required"`
Content string Content string

View File

@ -72,10 +72,11 @@ func (f *InstallForm) Validate(ctx *macaron.Context, errs binding.Errors) bindin
// RegisterForm form for registering // RegisterForm form for registering
type RegisterForm struct { type RegisterForm struct {
UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"` UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"`
Email string `binding:"Required;Email;MaxSize(254)"` Email string `binding:"Required;Email;MaxSize(254)"`
Password string `binding:"Required;MaxSize(255)"` Password string `binding:"Required;MaxSize(255)"`
Retype string Retype string
GRecaptchaResponse string `form:"g-recaptcha-response"`
} }
// Validate valideates the fields // Validate valideates the fields

View File

@ -22,8 +22,9 @@ func (f *SignInOpenIDForm) Validate(ctx *macaron.Context, errs binding.Errors) b
// SignUpOpenIDForm form for signin up with OpenID // SignUpOpenIDForm form for signin up with OpenID
type SignUpOpenIDForm struct { type SignUpOpenIDForm struct {
UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"` UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"`
Email string `binding:"Required;Email;MaxSize(254)"` Email string `binding:"Required;Email;MaxSize(254)"`
GRecaptchaResponse string `form:"g-recaptcha-response"`
} }
// Validate valideates the fields // Validate valideates the fields

View File

@ -37,12 +37,6 @@ func Toggle(options *ToggleOptions) macaron.Handler {
return return
} }
// Check non-logged users landing page.
if !ctx.IsSigned && ctx.Req.RequestURI == "/" && setting.LandingPageURL != setting.LandingPageHome {
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
return
}
// Redirect to dashboard if user tries to visit any non-login page. // Redirect to dashboard if user tries to visit any non-login page.
if options.SignOutRequired && ctx.IsSigned && ctx.Req.RequestURI != "/" { if options.SignOutRequired && ctx.IsSigned && ctx.Req.RequestURI != "/" {
ctx.Redirect(setting.AppSubURL + "/") ctx.Redirect(setting.AppSubURL + "/")

View File

@ -104,6 +104,11 @@ func (r *Repository) CanUseTimetracker(issue *models.Issue, user *models.User) b
r.IsWriter() || issue.IsPoster(user.ID) || isAssigned) r.IsWriter() || issue.IsPoster(user.ID) || isAssigned)
} }
// CanCreateIssueDependencies returns whether or not a user can create dependencies.
func (r *Repository) CanCreateIssueDependencies(user *models.User) bool {
return r.Repository.IsDependenciesEnabled() && r.IsWriter()
}
// GetCommitsCount returns cached commit count for current view // GetCommitsCount returns cached commit count for current view
func (r *Repository) GetCommitsCount() (int64, error) { func (r *Repository) GetCommitsCount() (int64, error) {
var contextName string var contextName string

View File

@ -85,9 +85,12 @@ type link struct {
var oidRegExp = regexp.MustCompile(`^[A-Fa-f0-9]+$`) var oidRegExp = regexp.MustCompile(`^[A-Fa-f0-9]+$`)
func isOidValid(oid string) bool {
return oidRegExp.MatchString(oid)
}
// ObjectOidHandler is the main request routing entry point into LFS server functions // ObjectOidHandler is the main request routing entry point into LFS server functions
func ObjectOidHandler(ctx *context.Context) { func ObjectOidHandler(ctx *context.Context) {
if !setting.LFS.StartServer { if !setting.LFS.StartServer {
writeStatus(ctx, 404) writeStatus(ctx, 404)
return return
@ -110,6 +113,11 @@ func ObjectOidHandler(ctx *context.Context) {
} }
func getAuthenticatedRepoAndMeta(ctx *context.Context, rv *RequestVars, requireWrite bool) (*models.LFSMetaObject, *models.Repository) { func getAuthenticatedRepoAndMeta(ctx *context.Context, rv *RequestVars, requireWrite bool) (*models.LFSMetaObject, *models.Repository) {
if !isOidValid(rv.Oid) {
writeStatus(ctx, 404)
return nil, nil
}
repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo) repository, err := models.GetRepositoryByOwnerAndName(rv.User, rv.Repo)
if err != nil { if err != nil {
log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err) log.Debug("Could not find repository: %s/%s - %s", rv.User, rv.Repo, err)
@ -222,7 +230,7 @@ func PostHandler(ctx *context.Context) {
return return
} }
if !oidRegExp.MatchString(rv.Oid) { if !isOidValid(rv.Oid) {
writeStatus(ctx, 404) writeStatus(ctx, 404)
return return
} }
@ -249,7 +257,6 @@ func PostHandler(ctx *context.Context) {
// BatchHandler provides the batch api // BatchHandler provides the batch api
func BatchHandler(ctx *context.Context) { func BatchHandler(ctx *context.Context) {
if !setting.LFS.StartServer { if !setting.LFS.StartServer {
writeStatus(ctx, 404) writeStatus(ctx, 404)
return return
@ -266,6 +273,10 @@ func BatchHandler(ctx *context.Context) {
// Create a response object // Create a response object
for _, object := range bv.Objects { for _, object := range bv.Objects {
if !isOidValid(object.Oid) {
continue
}
repository, err := models.GetRepositoryByOwnerAndName(object.User, object.Repo) repository, err := models.GetRepositoryByOwnerAndName(object.User, object.Repo)
if err != nil { if err != nil {
@ -292,12 +303,10 @@ func BatchHandler(ctx *context.Context) {
continue continue
} }
if oidRegExp.MatchString(object.Oid) { // Object is not found
// Object is not found meta, err = models.NewLFSMetaObject(&models.LFSMetaObject{Oid: object.Oid, Size: object.Size, RepositoryID: repository.ID})
meta, err = models.NewLFSMetaObject(&models.LFSMetaObject{Oid: object.Oid, Size: object.Size, RepositoryID: repository.ID}) if err == nil {
if err == nil { responseObjects = append(responseObjects, Represent(object, meta, meta.Existing, !contentStore.Exists(meta)))
responseObjects = append(responseObjects, Represent(object, meta, meta.Existing, !contentStore.Exists(meta)))
}
} }
} }

58
modules/markup/csv/csv.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package markup
import (
"bytes"
"encoding/csv"
"html"
"io"
"code.gitea.io/gitea/modules/markup"
)
func init() {
markup.RegisterParser(Parser{})
}
// Parser implements markup.Parser for orgmode
type Parser struct {
}
// Name implements markup.Parser
func (Parser) Name() string {
return "csv"
}
// Extensions implements markup.Parser
func (Parser) Extensions() []string {
return []string{".csv"}
}
// Render implements markup.Parser
func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
rd := csv.NewReader(bytes.NewReader(rawBytes))
var tmpBlock bytes.Buffer
tmpBlock.WriteString(`<table class="table">`)
for {
fields, err := rd.Read()
if err == io.EOF {
break
}
if err != nil {
continue
}
tmpBlock.WriteString("<tr>")
for _, field := range fields {
tmpBlock.WriteString("<td>")
tmpBlock.WriteString(html.EscapeString(field))
tmpBlock.WriteString("</td>")
}
tmpBlock.WriteString("<tr>")
}
tmpBlock.WriteString("</table>")
return tmpBlock.Bytes()
}

View File

@ -0,0 +1,25 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package markup
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRenderCSV(t *testing.T) {
var parser Parser
var kases = map[string]string{
"a": "<table class=\"table\"><tr><td>a</td><tr></table>",
"1,2": "<table class=\"table\"><tr><td>1</td><td>2</td><tr></table>",
"<br/>": "<table class=\"table\"><tr><td>&lt;br/&gt;</td><tr></table>",
}
for k, v := range kases {
res := parser.Render([]byte(k), "", nil, false)
assert.EqualValues(t, v, string(res))
}
}

View File

@ -469,6 +469,9 @@ func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) {
} else { } else {
link = strings.Replace(link, " ", "-", -1) link = strings.Replace(link, " ", "-", -1)
} }
if !strings.Contains(link, "/") {
link = url.PathEscape(link)
}
} }
urlPrefix := ctx.urlPrefix urlPrefix := ctx.urlPrefix
if image { if image {

View File

@ -82,12 +82,18 @@ func TestRender_ShortLinks(t *testing.T) {
rawtree := util.URLJoin(AppSubURL, "raw", "master") rawtree := util.URLJoin(AppSubURL, "raw", "master")
url := util.URLJoin(tree, "Link") url := util.URLJoin(tree, "Link")
otherURL := util.URLJoin(tree, "Other-Link") otherURL := util.URLJoin(tree, "Other-Link")
encodedURL := util.URLJoin(tree, "Link%3F")
imgurl := util.URLJoin(rawtree, "Link.jpg") imgurl := util.URLJoin(rawtree, "Link.jpg")
otherImgurl := util.URLJoin(rawtree, "Link+Other.jpg") otherImgurl := util.URLJoin(rawtree, "Link+Other.jpg")
encodedImgurl := util.URLJoin(rawtree, "Link+%23.jpg")
notencodedImgurl := util.URLJoin(rawtree, "some", "path", "Link+#.jpg")
urlWiki := util.URLJoin(AppSubURL, "wiki", "Link") urlWiki := util.URLJoin(AppSubURL, "wiki", "Link")
otherURLWiki := util.URLJoin(AppSubURL, "wiki", "Other-Link") otherURLWiki := util.URLJoin(AppSubURL, "wiki", "Other-Link")
encodedURLWiki := util.URLJoin(AppSubURL, "wiki", "Link%3F")
imgurlWiki := util.URLJoin(AppSubURL, "wiki", "raw", "Link.jpg") imgurlWiki := util.URLJoin(AppSubURL, "wiki", "raw", "Link.jpg")
otherImgurlWiki := util.URLJoin(AppSubURL, "wiki", "raw", "Link+Other.jpg") otherImgurlWiki := util.URLJoin(AppSubURL, "wiki", "raw", "Link+Other.jpg")
encodedImgurlWiki := util.URLJoin(AppSubURL, "wiki", "raw", "Link+%23.jpg")
notencodedImgurlWiki := util.URLJoin(AppSubURL, "wiki", "raw", "some", "path", "Link+#.jpg")
favicon := "http://google.com/favicon.ico" favicon := "http://google.com/favicon.ico"
test( test(
@ -134,4 +140,24 @@ func TestRender_ShortLinks(t *testing.T) {
"[[Link]] [[Other Link]]", "[[Link]] [[Other Link]]",
`<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a></p>`, `<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a></p>`,
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a></p>`) `<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a></p>`)
test(
"[[Link?]]",
`<p><a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`,
`<p><a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`)
test(
"[[Link]] [[Other Link]] [[Link?]]",
`<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherURL+`" rel="nofollow">Other Link</a> <a href="`+encodedURL+`" rel="nofollow">Link?</a></p>`,
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherURLWiki+`" rel="nofollow">Other Link</a> <a href="`+encodedURLWiki+`" rel="nofollow">Link?</a></p>`)
test(
"[[Link #.jpg]]",
`<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`"/></a></p>`,
`<p><a href="`+encodedImgurlWiki+`" rel="nofollow"><img src="`+encodedImgurlWiki+`"/></a></p>`)
test(
"[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]",
`<p><a href="`+encodedImgurl+`" rel="nofollow"><img src="`+encodedImgurl+`" title="Title" alt="AltName"/></a></p>`,
`<p><a href="`+encodedImgurlWiki+`" rel="nofollow"><img src="`+encodedImgurlWiki+`" title="Title" alt="AltName"/></a></p>`)
test(
"[[some/path/Link #.jpg]]",
`<p><a href="`+notencodedImgurl+`" rel="nofollow"><img src="`+notencodedImgurl+`"/></a></p>`,
`<p><a href="`+notencodedImgurlWiki+`" rel="nofollow"><img src="`+notencodedImgurlWiki+`"/></a></p>`)
} }

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