Compare commits
241 Commits
gitroast
...
ux-registe
Author | SHA1 | Date |
---|---|---|
AJ ONeal | 2bdc146bcf | |
SagePtr | 378af8ea88 | |
SagePtr | c6daee6da6 | |
Jonas Franz | 94cd7bb25b | |
Chih-Hsuan Yen | d7ca839c67 | |
Dane | 513db27377 | |
AJ ONeal | 2b8dc17db7 | |
GiteaBot | 2e2eacf62a | |
Mura Li | dba955be7c | |
GiteaBot | b8d048fa0d | |
Lauris BH | ab5b245182 | |
SagePtr | fc0001caa1 | |
Lauris BH | 81702e6ec9 | |
Iwasa Kazmi | 6780661192 | |
Lanre Adelowo | e6d54d511d | |
silverwind | 31d5488059 | |
GiteaBot | 39735723f5 | |
flufmonster | b3393b5ca7 | |
GiteaBot | 79b4d4729c | |
Lauris BH | 4092b32bad | |
zeripath | 36e7cb9755 | |
Wyall | dab02b80fd | |
OvermindDL1 | 07af31d004 | |
GiteaBot | 364c029246 | |
SagePtr | 043ab2cd59 | |
GiteaBot | 8b113cd8b6 | |
bugreport0 | 91b164c778 | |
GiteaBot | 552d8d3a4e | |
crito | d6b97c8557 | |
Lauris BH | 4befec242a | |
nubenum | 756eafaaf6 | |
Lanre Adelowo | acb6f8a518 | |
Bo-Yi Wu | f6eb669b51 | |
GiteaBot | 3e76e7826e | |
Girish Ramakrishnan | 661fd901bd | |
Lanre Adelowo | 2a6d3ba058 | |
linweijie2012 | f98040ad50 | |
Lanre Adelowo | 126ba796dc | |
SagePtr | 10a2a904d7 | |
Bo-Yi Wu | ea20adaa84 | |
GiteaBot | 7dd93b2441 | |
techknowlogick | e91a2cf2ea | |
Drew Bowering | 8e3e59fdb8 | |
GiteaBot | b5b39a56ad | |
Fluf | 08ed515b62 | |
GiteaBot | c64c595acc | |
techknowlogick | f1ad4bb3d5 | |
Lanre Adelowo | 8ee9922fe6 | |
B-OnTheGo | e47df0b301 | |
Bobonium | 387a4b09c1 | |
GiteaBot | 15ebe4b853 | |
Lanre Adelowo | be48397945 | |
GiteaBot | 6e03390aa8 | |
Toni Villena | eea76a5241 | |
vz | e9dbfc70b7 | |
GiteaBot | 1dee960b45 | |
Lewis Cowles | c43399cad8 | |
zeripath | d293a2b9d6 | |
Lukas Bestle | e6a03813d4 | |
Lauris BH | fab7937c62 | |
Lauris BH | fa4663e61e | |
SagePtr | bf55276189 | |
techknowlogick | 13e8a0fe56 | |
SagePtr | 303d7f7e9c | |
Max Wittig | 3c6cc56143 | |
Matthias Kappeller | bd1bf2a072 | |
maiki | a5cc3a9baf | |
Lunny Xiao | 061b1aa2d4 | |
Nicolas Lenz | 668a477c69 | |
Lunny Xiao | e48df3ee47 | |
techknowlogick | a4ee5627ed | |
GiteaBot | 01fd05a5cb | |
Bernhard Fröhlich | 83d1173634 | |
GiteaBot | aad5cccec8 | |
SagePtr | 9500d394ec | |
Najib Idrissi | b9ae16d15e | |
techknowlogick | fcea86877f | |
GiteaBot | a938ddf704 | |
techknowlogick | 080428b2bf | |
GiteaBot | 179123de35 | |
SagePtr | 74d65b5b5b | |
Joel da Rosa | 34831afaa7 | |
Piotr Orzechowski | 51c3b4b4bf | |
Piotr Orzechowski | 56d931aeac | |
SagePtr | 4ae5a54c1f | |
Deoren Moor | b63e2e0ded | |
EnricoFerro | 0a24f5cac8 | |
techknowlogick | 194a11eb11 | |
EnricoFerro | 912953e82a | |
Russell Aunger | 127f477056 | |
GiteaBot | 0dac1ff677 | |
Lanre Adelowo | f766e9713b | |
GiteaBot | 3c39b6351c | |
Lanre Adelowo | 33bc2ebdfa | |
Lanre Adelowo | 6ca8fbd2f9 | |
GiteaBot | 05dcfcfc33 | |
Fluf | b82c14b3d2 | |
Lanre Adelowo | 6c1a31ffaa | |
Julien Tant | fa93857117 | |
Lanre Adelowo | a6cdda115d | |
Tosone | b1ad5734c6 | |
GiteaBot | 1b2aff02a2 | |
SagePtr | 50ce19eff7 | |
GiteaBot | 8273479b41 | |
Andrew Phillips | b30f6b4099 | |
GiteaBot | f24ba27d79 | |
SagePtr | 8f86c43b90 | |
GiteaBot | bc06ab4a31 | |
Lauris BH | 92466129ec | |
SagePtr | 0449330dbc | |
nemoinho | ca112f0a04 | |
GiteaBot | 03e558c29b | |
Julien Tant | e6777f7b9d | |
GiteaBot | df0eb7372c | |
Julien Tant | 7781e8cef2 | |
Lunny Xiao | 52c2cb15db | |
SagePtr | a4fa6bbc89 | |
Lauris BH | 6e68b61479 | |
techknowlogick | ce7c64c7e1 | |
Lanre Adelowo | 3422077a89 | |
Lauris BH | 7fbdd4f2ac | |
SagePtr | 3ad5399a31 | |
Lanre Adelowo | 69a855f3d4 | |
SagePtr | 4a7de87071 | |
techknowlogick | 5dfe8b4340 | |
Lunny Xiao | 8da45ae2b3 | |
nemoinho | 0e04a2091a | |
Lunny Xiao | 578cf52ce5 | |
techknowlogick | 0e464995ce | |
Piotr Orzechowski | 152c6af97e | |
GiteaBot | 00bd6277a8 | |
Lanre Adelowo | 3b51c4f3fb | |
SagePtr | 32145b6de8 | |
GiteaBot | 67a8688538 | |
Jerry Jacobs | b1bc08e268 | |
GiteaBot | ed3589f429 | |
Lauris BH | 5ae8408725 | |
GiteaBot | a42900c17f | |
Lanre Adelowo | 0df7cab4fb | |
Lanre Adelowo | 59b10e66f7 | |
Lanre Adelowo | c7a6ee5c0b | |
GiteaBot | 7cb1c1cf20 | |
Dennis Menschel | 9c0a374f3f | |
GiteaBot | 9ea327f1f7 | |
Lauris BH | 6e64f9db8e | |
Lunny Xiao | 9c354a539a | |
Allen Wild | c40f5d2d4c | |
Dingjun | 819f50ccd5 | |
Julien Tant | ced08d18a4 | |
Benedikt Kroll | d57233680b | |
Lanre Adelowo | b11ddd8d4c | |
GiteaBot | e277b3e0e4 | |
techknowlogick | 895e538836 | |
Piotr Orzechowski | a74426d631 | |
GiteaBot | 412583a3f2 | |
SagePtr | c1224124ea | |
GiteaBot | 5927599e43 | |
techknowlogick | adf3f004b6 | |
SagePtr | ac968c3c6f | |
Lukas Treyer | be4ec0cbba | |
GiteaBot | a847d16f5b | |
kolaente | 8d1ad55598 | |
William Le Pommelet | f847884d16 | |
Lauris BH | 1b9b894731 | |
GiteaBot | 7a30208e05 | |
Lanre Adelowo | 630f234223 | |
GiteaBot | 32f25598b3 | |
Lanre Adelowo | 4bf0cae1f5 | |
GiteaBot | d0fef4395f | |
Michael Kuhn | 344dc07239 | |
Kjell Kvinge | ba358ecbf5 | |
Lauris BH | ae9dd239fb | |
GiteaBot | ec43e5619b | |
Kjell Kvinge | bed623600d | |
GiteaBot | 5fa403c874 | |
Lunny Xiao | 0bb1c84208 | |
bugreport0 | b174817b6a | |
GiteaBot | cc9fa062e6 | |
Lauris BH | 0c59edaafa | |
GiteaBot | 1e2da5d396 | |
techknowlogick | 492ec97a46 | |
Lunny Xiao | d1337299e2 | |
GiteaBot | 9ca8aaecb4 | |
Joel da Rosa | 91373901f6 | |
techknowlogick | fe78154895 | |
kolaente | 1bff02de55 | |
Mahmoud Al-Qudsi | 7be5935c55 | |
kolaente | ef6813abc9 | |
GiteaBot | 55d9ddf24a | |
Alexey Terentyev | ca474af3c6 | |
GiteaBot | 8e103d3e76 | |
techknowlogick | a7c5e58635 | |
GiteaBot | ca8c7bb2b5 | |
Joel da Rosa | aa27cbf229 | |
GiteaBot | 6813640d2f | |
Lauris BH | e95417ea2c | |
Lauris BH | 659bc727bd | |
Thomas Boerger | e6b51200ab | |
Lauris BH | a7f90905df | |
Thomas Boerger | 8afd500c48 | |
Thomas Boerger | 8b21cdba78 | |
Thomas Boerger | 3e950ef112 | |
silverwind | c55caeaf0c | |
Thomas Boerger | d84490a0df | |
Lauris BH | c2ec38f9b7 | |
GiteaBot | 61f4ad2fc5 | |
Niclas Kroon | c9687c036d | |
GiteaBot | 3e445cce06 | |
BNolet | 80169460ec | |
GiteaBot | efd202f40f | |
techknowlogick | ab55ca7ebd | |
Lunny Xiao | 1675fc4301 | |
techknowlogick | 61b40520ba | |
techknowlogick | f1d6a1fffc | |
GiteaBot | 28c1c90230 | |
Jonas Franz | 2a60e72fcd | |
GiteaBot | cfb76cd99e | |
Guido Diepen | 7c943b1cad | |
Lanre Adelowo | 5bc8782d33 | |
Nicolas Da Mutten | e07d3ad0fc | |
Fluf | f035dcd4f2 | |
Lauris BH | 54fedd4070 | |
GiteaBot | 07063e3e11 | |
Lauris BH | 69e2ab1611 | |
GiteaBot | 4eae810d63 | |
Lauris BH | 4a8ee0b5cc | |
Lunny Xiao | b46066f17c | |
GiteaBot | 95f0f62ea4 | |
Antoine GIRARD | 74f9f98f78 | |
Matthew Richardson | 2e3475f02c | |
GiteaBot | df77ad31d5 | |
techknowlogick | c3bbf43970 | |
GiteaBot | fc53f95f23 | |
Tao Wang | 823318bfbe | |
tarelda | 5676f60cba | |
GiteaBot | 3fed13b0ee | |
cezar97 | 51ba3df5ff | |
ucodi | 4b654ad17f | |
Pofilo | 5d1a6382b6 | |
Clar Charr | c71ee33057 | |
techknowlogick | 9d4c1ddfa1 |
48
.drone.yml
48
.drone.yml
|
@ -22,7 +22,7 @@ pipeline:
|
||||||
branch: [ master ]
|
branch: [ master ]
|
||||||
|
|
||||||
update-translations:
|
update-translations:
|
||||||
image: alpine:3.6
|
image: alpine:3.7
|
||||||
commands:
|
commands:
|
||||||
- mv ./options/locale/locale_en-US.ini ./options/
|
- mv ./options/locale/locale_en-US.ini ./options/
|
||||||
- sed -i -e 's/="/=/g' -e 's/"$$//g' ./options/locale/*.ini
|
- sed -i -e 's/="/=/g' -e 's/"$$//g' ./options/locale/*.ini
|
||||||
|
@ -56,7 +56,7 @@ pipeline:
|
||||||
event: [ push, tag, pull_request ]
|
event: [ push, tag, pull_request ]
|
||||||
|
|
||||||
build-without-gcc:
|
build-without-gcc:
|
||||||
image: golang:1.8
|
image: golang:1.9
|
||||||
pull: true
|
pull: true
|
||||||
commands:
|
commands:
|
||||||
- go build -o gitea_no_gcc # test if build succeeds without the sqlite tag
|
- go build -o gitea_no_gcc # test if build succeeds without the sqlite tag
|
||||||
|
@ -64,7 +64,7 @@ pipeline:
|
||||||
event: [ push, tag, pull_request ]
|
event: [ push, tag, pull_request ]
|
||||||
|
|
||||||
build:
|
build:
|
||||||
image: golang:1.10
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
environment:
|
environment:
|
||||||
TAGS: bindata sqlite
|
TAGS: bindata sqlite
|
||||||
|
@ -75,7 +75,7 @@ pipeline:
|
||||||
- make lint
|
- make lint
|
||||||
- make fmt-check
|
- make fmt-check
|
||||||
- make swagger-check
|
- make swagger-check
|
||||||
# - make swagger-validate
|
- make swagger-validate
|
||||||
- make misspell-check
|
- make misspell-check
|
||||||
- make test-vendor
|
- make test-vendor
|
||||||
- make build
|
- make build
|
||||||
|
@ -83,7 +83,7 @@ pipeline:
|
||||||
event: [ push, tag, pull_request ]
|
event: [ push, tag, pull_request ]
|
||||||
|
|
||||||
test:
|
test:
|
||||||
image: golang:1.10
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
group: test
|
group: test
|
||||||
environment:
|
environment:
|
||||||
|
@ -95,7 +95,7 @@ pipeline:
|
||||||
branch: [ master ]
|
branch: [ master ]
|
||||||
|
|
||||||
test:
|
test:
|
||||||
image: golang:1.10
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
group: test
|
group: test
|
||||||
environment:
|
environment:
|
||||||
|
@ -107,7 +107,7 @@ pipeline:
|
||||||
branch: [ release/* ]
|
branch: [ release/* ]
|
||||||
|
|
||||||
test:
|
test:
|
||||||
image: golang:1.10
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
group: test
|
group: test
|
||||||
environment:
|
environment:
|
||||||
|
@ -130,7 +130,7 @@ pipeline:
|
||||||
# event: [ push, tag, pull_request ]
|
# event: [ push, tag, pull_request ]
|
||||||
|
|
||||||
test-mysql:
|
test-mysql:
|
||||||
image: golang:1.10
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
group: test
|
group: test
|
||||||
environment:
|
environment:
|
||||||
|
@ -145,7 +145,7 @@ pipeline:
|
||||||
branch: [ master ]
|
branch: [ master ]
|
||||||
|
|
||||||
test-mysql:
|
test-mysql:
|
||||||
image: golang:1.10
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
group: test
|
group: test
|
||||||
environment:
|
environment:
|
||||||
|
@ -159,7 +159,7 @@ pipeline:
|
||||||
event: [ tag ]
|
event: [ tag ]
|
||||||
|
|
||||||
test-pgsql:
|
test-pgsql:
|
||||||
image: golang:1.10
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
group: test
|
group: test
|
||||||
environment:
|
environment:
|
||||||
|
@ -173,7 +173,7 @@ pipeline:
|
||||||
event: [ push, tag, pull_request ]
|
event: [ push, tag, pull_request ]
|
||||||
|
|
||||||
generate-coverage:
|
generate-coverage:
|
||||||
image: golang:1.10
|
image: golang:1.11
|
||||||
pull: true
|
pull: true
|
||||||
environment:
|
environment:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
|
@ -203,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:
|
||||||
|
@ -212,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 ]
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
11
Gopkg.toml
11
Gopkg.toml
|
@ -15,7 +15,7 @@ ignored = ["google.golang.org/appengine*"]
|
||||||
name = "code.gitea.io/sdk"
|
name = "code.gitea.io/sdk"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
revision = "9f005a07e0d31d45e6656d241bb5c0f2efd4bc94"
|
revision = "12dd70caea0268ac0d6c2707d0611ef601e7c64e"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
|
@ -30,16 +30,15 @@ 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 = "ad69f7d8f0861a29438154bb0a20b60501298480"
|
revision = "ad69f7d8f0861a29438154bb0a20b60501298480"
|
||||||
|
|
||||||
|
[[override]]
|
||||||
|
name = "github.com/go-sql-driver/mysql"
|
||||||
|
revision = "d523deb1b23d913de5bdada721a6071e71283618"
|
||||||
|
|
||||||
[[override]]
|
[[override]]
|
||||||
name = "github.com/gorilla/mux"
|
name = "github.com/gorilla/mux"
|
||||||
revision = "757bef944d0f21880861c2dd9c871ca543023cba"
|
revision = "757bef944d0f21880861c2dd9c871ca543023cba"
|
||||||
|
|
|
@ -22,5 +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)
|
Alexey Terentyev <axifnx@gmail.com> (@axifive)
|
||||||
|
Lanre Adelowo <yo@lanre.wtf> (@adelowo)
|
||||||
|
|
19
Makefile
19
Makefile
|
@ -42,6 +42,10 @@ TAGS ?=
|
||||||
|
|
||||||
TMPDIR := $(shell mktemp -d 2>/dev/null || mktemp -d -t 'gitea-temp')
|
TMPDIR := $(shell mktemp -d 2>/dev/null || mktemp -d -t 'gitea-temp')
|
||||||
|
|
||||||
|
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
|
||||||
|
SWAGGER_SPEC_S_TMPL := s|"basePath":\s*"/api/v1"|"basePath": "{{AppSubUrl}}/api/v1"|g
|
||||||
|
SWAGGER_SPEC_S_JSON := s|"basePath":\s*"{{AppSubUrl}}/api/v1"|"basePath": "/api/v1"|g
|
||||||
|
|
||||||
TEST_MYSQL_HOST ?= mysql:3306
|
TEST_MYSQL_HOST ?= mysql:3306
|
||||||
TEST_MYSQL_DBNAME ?= testgitea
|
TEST_MYSQL_DBNAME ?= testgitea
|
||||||
TEST_MYSQL_USERNAME ?= root
|
TEST_MYSQL_USERNAME ?= root
|
||||||
|
@ -57,6 +61,9 @@ else
|
||||||
EXECUTABLE := gitea
|
EXECUTABLE := gitea
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
# $(call strip-suffix,filename)
|
||||||
|
strip-suffix = $(firstword $(subst ., ,$(1)))
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
@ -91,11 +98,12 @@ generate-swagger:
|
||||||
@hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
$(GO) get -u github.com/go-swagger/go-swagger/cmd/swagger; \
|
$(GO) get -u github.com/go-swagger/go-swagger/cmd/swagger; \
|
||||||
fi
|
fi
|
||||||
swagger generate spec -o ./public/swagger.v1.json
|
swagger generate spec -o './$(SWAGGER_SPEC)'
|
||||||
|
$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)'
|
||||||
|
|
||||||
.PHONY: swagger-check
|
.PHONY: swagger-check
|
||||||
swagger-check: generate-swagger
|
swagger-check: generate-swagger
|
||||||
@diff=$$(git diff public/swagger.v1.json); \
|
@diff=$$(git diff '$(SWAGGER_SPEC)'); \
|
||||||
if [ -n "$$diff" ]; then \
|
if [ -n "$$diff" ]; then \
|
||||||
echo "Please run 'make generate-swagger' and commit the result:"; \
|
echo "Please run 'make generate-swagger' and commit the result:"; \
|
||||||
echo "$${diff}"; \
|
echo "$${diff}"; \
|
||||||
|
@ -107,7 +115,9 @@ swagger-validate:
|
||||||
@hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
@hash swagger > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||||
$(GO) get -u github.com/go-swagger/go-swagger/cmd/swagger; \
|
$(GO) get -u github.com/go-swagger/go-swagger/cmd/swagger; \
|
||||||
fi
|
fi
|
||||||
swagger validate ./public/swagger.v1.json
|
$(SED_INPLACE) '$(SWAGGER_SPEC_S_JSON)' './$(SWAGGER_SPEC)'
|
||||||
|
swagger validate './$(SWAGGER_SPEC)'
|
||||||
|
$(SED_INPLACE) '$(SWAGGER_SPEC_S_TMPL)' './$(SWAGGER_SPEC)'
|
||||||
|
|
||||||
.PHONY: errcheck
|
.PHONY: errcheck
|
||||||
errcheck:
|
errcheck:
|
||||||
|
@ -303,7 +313,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}"; \
|
||||||
|
@ -313,6 +323,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:
|
||||||
|
|
|
@ -29,7 +29,7 @@ This project has been
|
||||||
|
|
||||||
From the root of the source tree, run:
|
From the root of the source tree, run:
|
||||||
|
|
||||||
make generate all
|
TAGS="bindata" make generate all
|
||||||
|
|
||||||
More info: https://docs.gitea.io/en-us/install-from-source/
|
More info: https://docs.gitea.io/en-us/install-from-source/
|
||||||
|
|
||||||
|
@ -90,6 +90,10 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
|
||||||
|
|
||||||
Gitea is pronounced [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY) as in "gi-tea" with a hard g.
|
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.
|
||||||
|
|
18
README_ZH.md
18
README_ZH.md
|
@ -11,16 +11,9 @@
|
||||||
[![GitHub release](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest)
|
[![GitHub release](https://img.shields.io/github/release/go-gitea/gitea.svg)](https://github.com/go-gitea/gitea/releases/latest)
|
||||||
[![Become a backer/sponsor of gitea](https://opencollective.com/gitea/tiers/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)|
|
|
||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
Gitea的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用Go作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了x86,amd64,还包括 ARM 和 PowerPC。
|
Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86,amd64,还包括 ARM 和 PowerPC。
|
||||||
|
|
||||||
如果您想试用一下,请访问 [在线Demo](https://try.gitea.io/)!
|
如果您想试用一下,请访问 [在线Demo](https://try.gitea.io/)!
|
||||||
|
|
||||||
|
@ -47,3 +40,12 @@ Fork -> Patch -> Push -> Pull Request
|
||||||
## 授权许可
|
## 授权许可
|
||||||
|
|
||||||
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/master/LICENSE) 文件中。
|
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://github.com/go-gitea/gitea/blob/master/LICENSE) 文件中。
|
||||||
|
|
||||||
|
## 截图
|
||||||
|
|
||||||
|
| | | |
|
||||||
|
|:---:|:---:|:---:|
|
||||||
|
|![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)|
|
||||||
|
|
286
cmd/admin.go
286
cmd/admin.go
|
@ -7,9 +7,12 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
"code.gitea.io/git"
|
"code.gitea.io/git"
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/auth/oauth2"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
|
@ -26,6 +29,7 @@ var (
|
||||||
subcmdChangePassword,
|
subcmdChangePassword,
|
||||||
subcmdRepoSyncReleases,
|
subcmdRepoSyncReleases,
|
||||||
subcmdRegenerate,
|
subcmdRegenerate,
|
||||||
|
subcmdAuth,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,6 +125,121 @@ var (
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subcmdAuth = cli.Command{
|
||||||
|
Name: "auth",
|
||||||
|
Usage: "Modify external auth providers",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
microcmdAuthAddOauth,
|
||||||
|
microcmdAuthUpdateOauth,
|
||||||
|
microcmdAuthList,
|
||||||
|
microcmdAuthDelete,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
microcmdAuthList = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "List auth sources",
|
||||||
|
Action: runListAuth,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "config, c",
|
||||||
|
Value: "custom/conf/app.ini",
|
||||||
|
Usage: "Custom configuration file path",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
idFlag = cli.Int64Flag{
|
||||||
|
Name: "id",
|
||||||
|
Usage: "ID of OAuth authentication source",
|
||||||
|
}
|
||||||
|
|
||||||
|
microcmdAuthDelete = cli.Command{
|
||||||
|
Name: "delete",
|
||||||
|
Usage: "Delete specific auth source",
|
||||||
|
Action: runDeleteAuth,
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "config, c",
|
||||||
|
Value: "custom/conf/app.ini",
|
||||||
|
Usage: "Custom configuration file path",
|
||||||
|
},
|
||||||
|
idFlag,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthCLIFlags = []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "config, c",
|
||||||
|
Value: "custom/conf/app.ini",
|
||||||
|
Usage: "Custom configuration file path",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Value: "",
|
||||||
|
Usage: "Application Name",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "provider",
|
||||||
|
Value: "",
|
||||||
|
Usage: "OAuth2 Provider",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key",
|
||||||
|
Value: "",
|
||||||
|
Usage: "Client ID (Key)",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "secret",
|
||||||
|
Value: "",
|
||||||
|
Usage: "Client Secret",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "auto-discover-url",
|
||||||
|
Value: "",
|
||||||
|
Usage: "OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider)",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "use-custom-urls",
|
||||||
|
Value: "false",
|
||||||
|
Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "custom-auth-url",
|
||||||
|
Value: "",
|
||||||
|
Usage: "Use a custom Authorization URL (option for GitLab/GitHub)",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "custom-token-url",
|
||||||
|
Value: "",
|
||||||
|
Usage: "Use a custom Token URL (option for GitLab/GitHub)",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "custom-profile-url",
|
||||||
|
Value: "",
|
||||||
|
Usage: "Use a custom Profile URL (option for GitLab/GitHub)",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "custom-email-url",
|
||||||
|
Value: "",
|
||||||
|
Usage: "Use a custom Email URL (option for GitHub)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
microcmdAuthUpdateOauth = cli.Command{
|
||||||
|
Name: "update-oauth",
|
||||||
|
Usage: "Update existing Oauth authentication source",
|
||||||
|
Action: runUpdateOauth,
|
||||||
|
Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
|
||||||
|
}
|
||||||
|
|
||||||
|
microcmdAuthAddOauth = cli.Command{
|
||||||
|
Name: "add-oauth",
|
||||||
|
Usage: "Add new Oauth authentication source",
|
||||||
|
Action: runAddOauth,
|
||||||
|
Flags: oauthCLIFlags,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func runChangePassword(c *cli.Context) error {
|
func runChangePassword(c *cli.Context) error {
|
||||||
|
@ -262,3 +381,170 @@ func runRegenerateKeys(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
return models.RewriteAllPublicKeys()
|
return models.RewriteAllPublicKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
|
||||||
|
var customURLMapping *oauth2.CustomURLMapping
|
||||||
|
if c.IsSet("use-custom-urls") {
|
||||||
|
customURLMapping = &oauth2.CustomURLMapping{
|
||||||
|
TokenURL: c.String("custom-token-url"),
|
||||||
|
AuthURL: c.String("custom-auth-url"),
|
||||||
|
ProfileURL: c.String("custom-profile-url"),
|
||||||
|
EmailURL: c.String("custom-email-url"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customURLMapping = nil
|
||||||
|
}
|
||||||
|
return &models.OAuth2Config{
|
||||||
|
Provider: c.String("provider"),
|
||||||
|
ClientID: c.String("key"),
|
||||||
|
ClientSecret: c.String("secret"),
|
||||||
|
OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
|
||||||
|
CustomURLMapping: customURLMapping,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAddOauth(c *cli.Context) error {
|
||||||
|
if c.IsSet("config") {
|
||||||
|
setting.CustomConf = c.String("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.CreateLoginSource(&models.LoginSource{
|
||||||
|
Type: models.LoginOAuth2,
|
||||||
|
Name: c.String("name"),
|
||||||
|
IsActived: true,
|
||||||
|
Cfg: parseOAuth2Config(c),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUpdateOauth(c *cli.Context) error {
|
||||||
|
if c.IsSet("config") {
|
||||||
|
setting.CustomConf = c.String("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.IsSet("id") {
|
||||||
|
return fmt.Errorf("--id flag is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
source, err := models.GetLoginSourceByID(c.Int64("id"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oAuth2Config := source.OAuth2()
|
||||||
|
|
||||||
|
if c.IsSet("name") {
|
||||||
|
source.Name = c.String("name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet("provider") {
|
||||||
|
oAuth2Config.Provider = c.String("provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet("key") {
|
||||||
|
oAuth2Config.ClientID = c.String("key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet("secret") {
|
||||||
|
oAuth2Config.ClientSecret = c.String("secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet("auto-discover-url") {
|
||||||
|
oAuth2Config.OpenIDConnectAutoDiscoveryURL = c.String("auto-discover-url")
|
||||||
|
}
|
||||||
|
|
||||||
|
// update custom URL mapping
|
||||||
|
var customURLMapping *oauth2.CustomURLMapping
|
||||||
|
|
||||||
|
if oAuth2Config.CustomURLMapping != nil {
|
||||||
|
customURLMapping.TokenURL = oAuth2Config.CustomURLMapping.TokenURL
|
||||||
|
customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
|
||||||
|
customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
|
||||||
|
customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
|
||||||
|
}
|
||||||
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
|
||||||
|
customURLMapping.TokenURL = c.String("custom-token-url")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-auth-url") {
|
||||||
|
customURLMapping.AuthURL = c.String("custom-auth-url")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-profile-url") {
|
||||||
|
customURLMapping.ProfileURL = c.String("custom-profile-url")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet("use-custom-urls") && c.IsSet("custom-email-url") {
|
||||||
|
customURLMapping.EmailURL = c.String("custom-email-url")
|
||||||
|
}
|
||||||
|
|
||||||
|
oAuth2Config.CustomURLMapping = customURLMapping
|
||||||
|
source.Cfg = oAuth2Config
|
||||||
|
|
||||||
|
if err := models.UpdateSource(source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runListAuth(c *cli.Context) error {
|
||||||
|
if c.IsSet("config") {
|
||||||
|
setting.CustomConf = c.String("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loginSources, err := models.LoginSources()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through each source and print
|
||||||
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', tabwriter.AlignRight)
|
||||||
|
fmt.Fprintf(w, "ID\tName\tType\tEnabled")
|
||||||
|
for _, source := range loginSources {
|
||||||
|
fmt.Fprintf(w, "%d\t%s\t%s\t%t", source.ID, source.Name, models.LoginNames[source.Type], source.IsActived)
|
||||||
|
}
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDeleteAuth(c *cli.Context) error {
|
||||||
|
if c.IsSet("config") {
|
||||||
|
setting.CustomConf = c.String("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.IsSet("id") {
|
||||||
|
return fmt.Errorf("--id flag is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initDB(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
source, err := models.GetLoginSourceByID(c.Int64("id"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = models.DeleteSource(source); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
16
cmd/serv.go
16
cmd/serv.go
|
@ -16,6 +16,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/pprof"
|
||||||
"code.gitea.io/gitea/modules/private"
|
"code.gitea.io/gitea/modules/private"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
@ -42,6 +43,9 @@ var CmdServ = cli.Command{
|
||||||
Value: "custom/conf/app.ini",
|
Value: "custom/conf/app.ini",
|
||||||
Usage: "Custom configuration file path",
|
Usage: "Custom configuration file path",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "enable-pprof",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +147,18 @@ func runServ(c *cli.Context) error {
|
||||||
username := strings.ToLower(rr[0])
|
username := strings.ToLower(rr[0])
|
||||||
reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
|
reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
|
||||||
|
|
||||||
|
if setting.EnablePprof || c.Bool("enable-pprof") {
|
||||||
|
if err := os.MkdirAll(setting.PprofDataPath, os.ModePerm); err != nil {
|
||||||
|
fail("Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCPUProfiler := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username)
|
||||||
|
defer func() {
|
||||||
|
stopCPUProfiler()
|
||||||
|
pprof.DumpMemProfileForUsername(setting.PprofDataPath, username)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
isWiki := false
|
isWiki := false
|
||||||
unitType := models.UnitTypeCode
|
unitType := models.UnitTypeCode
|
||||||
if strings.HasSuffix(reponame, ".wiki") {
|
if strings.HasSuffix(reponame, ".wiki") {
|
||||||
|
|
33
cmd/web.go
33
cmd/web.go
|
@ -5,6 +5,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -22,6 +23,7 @@ import (
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
context2 "github.com/gorilla/context"
|
context2 "github.com/gorilla/context"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
"golang.org/x/crypto/acme/autocert"
|
||||||
ini "gopkg.in/ini.v1"
|
ini "gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,6 +73,33 @@ func runHTTPRedirector() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler) error {
|
||||||
|
certManager := autocert.Manager{
|
||||||
|
Prompt: autocert.AcceptTOS,
|
||||||
|
HostPolicy: autocert.HostWhitelist(domain),
|
||||||
|
Cache: autocert.DirCache(directory),
|
||||||
|
Email: email,
|
||||||
|
}
|
||||||
|
go http.ListenAndServe(listenAddr+":"+setting.PortToRedirect, certManager.HTTPHandler(http.HandlerFunc(runLetsEncryptFallbackHandler))) // all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validatio happens here)
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: listenAddr,
|
||||||
|
Handler: m,
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
GetCertificate: certManager.GetCertificate,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return server.ListenAndServeTLS("", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLetsEncryptFallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" && r.Method != "HEAD" {
|
||||||
|
http.Error(w, "Use HTTPS", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target := setting.AppURL + r.URL.RequestURI()
|
||||||
|
http.Redirect(w, r, target, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
func runWeb(ctx *cli.Context) error {
|
func runWeb(ctx *cli.Context) error {
|
||||||
if ctx.IsSet("config") {
|
if ctx.IsSet("config") {
|
||||||
setting.CustomConf = ctx.String("config")
|
setting.CustomConf = ctx.String("config")
|
||||||
|
@ -143,6 +172,10 @@ func runWeb(ctx *cli.Context) error {
|
||||||
case setting.HTTP:
|
case setting.HTTP:
|
||||||
err = runHTTP(listenAddr, context2.ClearHandler(m))
|
err = runHTTP(listenAddr, context2.ClearHandler(m))
|
||||||
case setting.HTTPS:
|
case setting.HTTPS:
|
||||||
|
if setting.EnableLetsEncrypt {
|
||||||
|
err = runLetsEncrypt(listenAddr, setting.Domain, setting.LetsEncryptDirectory, setting.LetsEncryptEmail, context2.ClearHandler(m))
|
||||||
|
break
|
||||||
|
}
|
||||||
if setting.RedirectOtherPort {
|
if setting.RedirectOtherPort {
|
||||||
go runHTTPRedirector()
|
go runHTTPRedirector()
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,10 @@ FILE_MAX_SIZE = 3
|
||||||
; Max number of files per upload. Defaults to 5
|
; Max number of files per upload. Defaults to 5
|
||||||
MAX_FILES = 5
|
MAX_FILES = 5
|
||||||
|
|
||||||
|
[repository.pull-request]
|
||||||
|
; List of prefixes used in Pull Request title to mark them as Work In Progress
|
||||||
|
WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
; Number of repositories that are displayed on one explore page
|
; Number of repositories that are displayed on one explore page
|
||||||
EXPLORE_PAGING_NUM = 20
|
EXPLORE_PAGING_NUM = 20
|
||||||
|
@ -67,6 +71,10 @@ 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
|
||||||
|
; Number of line of codes shown for a code comment
|
||||||
|
CODE_COMMENT_LINES = 4
|
||||||
; 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,13 +83,15 @@ 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
|
||||||
USER_PAGING_NUM = 50
|
USER_PAGING_NUM = 50
|
||||||
; Number of repos that are displayed on one page
|
; Number of repos that are displayed on one page
|
||||||
REPO_PAGING_NUM = 50
|
REPO_PAGING_NUM = 50
|
||||||
; Number of notices that are displayed on in one page
|
; Number of notices that are displayed on one page
|
||||||
NOTICE_PAGING_NUM = 25
|
NOTICE_PAGING_NUM = 25
|
||||||
; Number of organizations that are displayed on one page
|
; Number of organizations that are displayed on one page
|
||||||
ORG_PAGING_NUM = 50
|
ORG_PAGING_NUM = 50
|
||||||
|
@ -181,6 +191,12 @@ STATIC_ROOT_PATH =
|
||||||
APP_DATA_PATH = data
|
APP_DATA_PATH = data
|
||||||
; Application level GZIP support
|
; Application level GZIP support
|
||||||
ENABLE_GZIP = false
|
ENABLE_GZIP = false
|
||||||
|
; Application profiling (memory and cpu)
|
||||||
|
; For "web" command it listens on localhost:6060
|
||||||
|
; For "serve" command it dumps to disk at PPROF_DATA_PATH as (cpuprofile|memprofile)_<username>_<temporary id>
|
||||||
|
ENABLE_PPROF = false
|
||||||
|
; PPROF_DATA_PATH, use an absolute path when you start gitea as service
|
||||||
|
PPROF_DATA_PATH = data/tmp/pprof
|
||||||
; Landing page, can be "home", "explore", or "organizations"
|
; Landing page, can be "home", "explore", or "organizations"
|
||||||
LANDING_PAGE = home
|
LANDING_PAGE = home
|
||||||
; Enables git-lfs support. true or false, default is false.
|
; Enables git-lfs support. true or false, default is false.
|
||||||
|
@ -207,9 +223,10 @@ NAME = gitea
|
||||||
USER = root
|
USER = root
|
||||||
; Use PASSWD = `your password` for quoting if you use special characters in the password.
|
; Use PASSWD = `your password` for quoting if you use special characters in the password.
|
||||||
PASSWD =
|
PASSWD =
|
||||||
; For "postgres" only, either "disable", "require" or "verify-full"
|
; For Postgres, either "disable" (default), "require", or "verify-full"
|
||||||
|
; For MySQL, either "false" (default), "true", or "skip-verify"
|
||||||
SSL_MODE = disable
|
SSL_MODE = disable
|
||||||
; For "sqlite3" and "tidb", use absolute path when you start gitea as service
|
; For "sqlite3" and "tidb", use an absolute path when you start gitea as service
|
||||||
PATH = data/gitea.db
|
PATH = data/gitea.db
|
||||||
; For "sqlite3" only. Query timeout
|
; For "sqlite3" only. Query timeout
|
||||||
SQLITE_TIMEOUT = 500
|
SQLITE_TIMEOUT = 500
|
||||||
|
@ -301,13 +318,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
|
||||||
|
@ -568,8 +594,8 @@ DEFAULT_INTERVAL = 8h
|
||||||
MIN_INTERVAL = 10m
|
MIN_INTERVAL = 10m
|
||||||
|
|
||||||
[api]
|
[api]
|
||||||
; Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.
|
; Enables Swagger. True or false; default is true.
|
||||||
ENABLE_SWAGGER_ENDPOINT = true
|
ENABLE_SWAGGER = true
|
||||||
; Max number of items in a page
|
; Max number of items in a page
|
||||||
MAX_RESPONSE_ITEMS = 50
|
MAX_RESPONSE_ITEMS = 50
|
||||||
|
|
||||||
|
|
|
@ -29,4 +29,3 @@ AllowUsers git
|
||||||
|
|
||||||
Banner none
|
Banner none
|
||||||
Subsystem sftp /usr/lib/ssh/sftp-server
|
Subsystem sftp /usr/lib/ssh/sftp-server
|
||||||
UsePrivilegeSeparation no
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
public/
|
public/
|
||||||
|
templates/swagger/v1_json.tmpl
|
||||||
themes/
|
themes/
|
||||||
|
|
|
@ -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"]
|
|
|
@ -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/
|
||||||
|
|
|
@ -73,3 +73,7 @@ using BasicAuth, as follows:
|
||||||
$ curl --request GET --url https://yourusername:yourpassword@gitea.your.host/api/v1/users/yourusername/tokens
|
$ curl --request GET --url https://yourusername:yourpassword@gitea.your.host/api/v1/users/yourusername/tokens
|
||||||
[{"name":"test","sha1":"..."},{"name":"dev","sha1":"..."}]
|
[{"name":"test","sha1":"..."},{"name":"dev","sha1":"..."}]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Sudo
|
||||||
|
|
||||||
|
The API allows admin users to sudo API requests as another user. Simply add either a `sudo=` parameter or `Sudo:` request header with the username of the user to sudo.
|
||||||
|
|
|
@ -63,11 +63,17 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
- `USE_COMPAT_SSH_URI`: **false**: Force ssh:// clone url instead of scp-style uri when
|
- `USE_COMPAT_SSH_URI`: **false**: Force ssh:// clone url instead of scp-style uri when
|
||||||
default SSH port is used.
|
default SSH port is used.
|
||||||
|
|
||||||
|
### Repository - Pull Request (`repository.pull-request`)
|
||||||
|
- `WORK_IN_PROGRESS_PREFIXES`: **WIP:,\[WIP\]**: List of prefixes used in Pull Request
|
||||||
|
title to mark them as Work In Progress
|
||||||
|
|
||||||
## UI (`ui`)
|
## UI (`ui`)
|
||||||
|
|
||||||
- `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`)
|
||||||
|
|
||||||
|
@ -119,6 +125,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
- `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.
|
||||||
|
- `ENABLE_LETSENCRYPT`: **false**: If enabled you must set `DOMAIN` to valid internet facing domain (ensure DNS is set and port 80 is accessible by letsencrypt validation server).
|
||||||
|
By using Lets Encrypt **you must consent** to their [terms of service](https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf).
|
||||||
|
- `LETSENCRYPT_ACCEPTTOS`: **false**: This is an explicit check that you accept the terms of service for Let's Encrypt.
|
||||||
|
- `LETSENCRYPT_DIRECTORY`: **https**: Directory that Letsencrypt will use to cache information such as certs and private keys.
|
||||||
|
- `LETSENCRYPT_EMAIL`: **email@example.com**: Email used by Letsencrypt to notify about problems with issued certificates. (No default)
|
||||||
|
|
||||||
## Database (`database`)
|
## Database (`database`)
|
||||||
|
|
||||||
|
@ -127,7 +138,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
- `NAME`: **gitea**: Database name.
|
- `NAME`: **gitea**: Database name.
|
||||||
- `USER`: **root**: Database username.
|
- `USER`: **root**: Database username.
|
||||||
- `PASSWD`: **\<empty\>**: Database user password. Use \`your password\` for quoting if you use special characters in the password.
|
- `PASSWD`: **\<empty\>**: Database user password. Use \`your password\` for quoting if you use special characters in the password.
|
||||||
- `SSL_MODE`: **disable**: For PostgreSQL only.
|
- `SSL_MODE`: **disable**: For PostgreSQL and MySQL only.
|
||||||
- `PATH`: **data/gitea.db**: For SQLite3 only, the database file path.
|
- `PATH`: **data/gitea.db**: For SQLite3 only, the database file path.
|
||||||
- `LOG_SQL`: **true**: Log the executed SQL.
|
- `LOG_SQL`: **true**: Log the executed SQL.
|
||||||
|
|
||||||
|
@ -177,7 +188,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`)
|
||||||
|
|
||||||
|
@ -202,8 +217,8 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
This is common on linux systems.
|
This is common on linux systems.
|
||||||
- Note that enabling sendmail will ignore all other `mailer` settings except `ENABLED`,
|
- Note that enabling sendmail will ignore all other `mailer` settings except `ENABLED`,
|
||||||
`FROM` and `SENDMAIL_PATH`.
|
`FROM` and `SENDMAIL_PATH`.
|
||||||
- `SENDMAIL_PATH`: **sendmail**: The location of sendmail on the operating system. (can be
|
- `SENDMAIL_PATH`: **sendmail**: The location of sendmail on the operating system (can be
|
||||||
command or full path)
|
command or full path).
|
||||||
|
|
||||||
## Cache (`cache`)
|
## Cache (`cache`)
|
||||||
|
|
||||||
|
@ -227,7 +242,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
`http://cn.gravatar.com/avatar/`.
|
`http://cn.gravatar.com/avatar/`.
|
||||||
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only.
|
- `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only.
|
||||||
- `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see
|
- `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see
|
||||||
http://www.libravatar.org)
|
[http://www.libravatar.org](http://www.libravatar.org)).
|
||||||
- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store local and cached files.
|
- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store local and cached files.
|
||||||
|
|
||||||
## Attachment (`attachment`)
|
## Attachment (`attachment`)
|
||||||
|
@ -279,10 +294,17 @@ 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.
|
||||||
- `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page
|
- `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page.
|
||||||
|
|
||||||
## i18n (`i18n`)
|
## i18n (`i18n`)
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -59,7 +59,7 @@ to override can be found in the `templates` directory of Gitea source. Override
|
||||||
making a copy of the file under `custom/templates` using a full path structure
|
making a copy of the file under `custom/templates` using a full path structure
|
||||||
matching source.
|
matching source.
|
||||||
|
|
||||||
Any statement contained inside `{{` and `}}` are Gitea's templete syntax and
|
Any statement contained inside `{{` and `}}` are Gitea's template syntax and
|
||||||
shouldn't be touched without fully understanding these components.
|
shouldn't be touched without fully understanding these components.
|
||||||
|
|
||||||
### Adding links and tabs
|
### Adding links and tabs
|
||||||
|
@ -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.
|
||||||
|
|
|
@ -66,4 +66,3 @@ For documentation about each of the variables available, refer to the
|
||||||
## Miscellaneous
|
## Miscellaneous
|
||||||
|
|
||||||
* `SKIP_MINWINSVC`: If set to 1, do not run as a service on Windows.
|
* `SKIP_MINWINSVC`: If set to 1, do not run as a service on Windows.
|
||||||
* `ZOOKEEPER_PATH`: [Zookeeper](http://zookeeper.apache.org/) jar file path
|
|
||||||
|
|
|
@ -37,6 +37,9 @@ _Symbols used in table:_
|
||||||
| Multiple OS support | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ |
|
| Multiple OS support | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ |
|
||||||
| Easy upgrade process | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ |
|
| Easy upgrade process | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ |
|
||||||
| Markdown support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| Markdown support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
| Orgmode support | ✓ | ✘ | ✓ | ✘ | ✘ | ✘ | ? |
|
||||||
|
| CSV support | ✓ | ✘ | ✓ | ✘ | ✘ | ✓ | ? |
|
||||||
|
| Third-party render tool support | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | ? |
|
||||||
| Static Git-powered pages | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Static Git-powered pages | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Integrated Git-powered wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| Integrated Git-powered wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
| Deploy Tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| Deploy Tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
@ -53,7 +56,7 @@ _Symbols used in table:_
|
||||||
|---------|-------|------|-----------|-----------|-----------|-----------|--------------|
|
|---------|-------|------|-----------|-----------|-----------|-----------|--------------|
|
||||||
| Repository topics | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Repository topics | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Repository code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| Repository code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| Global code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| Global code search | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ |
|
||||||
| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | ⁄ | ✓ |
|
| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | ⁄ | ✓ |
|
||||||
| Group Milestones | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
| Group Milestones | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Granular user roles (Code, Issues, Wiki etc) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
| Granular user roles (Code, Issues, Wiki etc) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
||||||
|
@ -74,7 +77,7 @@ _Symbols used in table:_
|
||||||
| Issue templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Issue templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Labels | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Labels | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Time tracking | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Time tracking | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Multiple assignees for issues | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Multiple assignees for issues | ✓ | ✘ | ✓ | ✘ | ✓ | ✘ | ✘ |
|
||||||
| Related issues | ✘ | ✘ | ⁄ | ✘ | ✓ | ✘ | ✘ |
|
| Related issues | ✘ | ✘ | ⁄ | ✘ | ✓ | ✘ | ✘ |
|
||||||
| Confidential issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
| Confidential issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
|
@ -84,6 +87,7 @@ _Symbols used in table:_
|
||||||
| Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
| Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
| Global issue search | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| Global issue search | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
|
| Issue dependency | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | ✘ |
|
||||||
|
|
||||||
#### Pull/Merge requests
|
#### Pull/Merge requests
|
||||||
|
|
||||||
|
@ -92,8 +96,8 @@ _Symbols used in table:_
|
||||||
| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| Squash merging | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ |
|
| Squash merging | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ |
|
||||||
| Rebase merging | ✓ | ✓ | ✓ | ✘ | ⁄ | ✘ | ✓ |
|
| Rebase merging | ✓ | ✓ | ✓ | ✘ | ⁄ | ✘ | ✓ |
|
||||||
| Pull/Merge request inline comments | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
| Pull/Merge request inline comments | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| Pull/Merge request approval | ✘ | ✘ | ⁄ | ✓ | ✓ | ✓ | ✓ |
|
| Pull/Merge request approval | ✓ | ✘ | ⁄ | ✓ | ✓ | ✓ | ✓ |
|
||||||
| Merge conflict resolution | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| Merge conflict resolution | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
| Restrict push and merge access to certain users | ✓ | ✘ | ✓ | ⁄ | ✓ | ✓ | ✓ |
|
| Restrict push and merge access to certain users | ✓ | ✘ | ✓ | ⁄ | ✓ | ✓ | ✓ |
|
||||||
| Revert specific commits or a merge request | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
| Revert specific commits or a merge request | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ |
|
||||||
|
|
|
@ -21,10 +21,18 @@ the destination platform from the [downloads page](https://dl.gitea.io/gitea), c
|
||||||
the URL and replace the URL within the commands below:
|
the URL and replace the URL within the commands below:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
wget -O gitea https://dl.gitea.io/gitea/1.4.2/gitea-1.4.2-linux-amd64
|
wget -O gitea https://dl.gitea.io/gitea/1.5.0/gitea-1.5.0-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
|
||||||
|
@ -82,6 +90,20 @@ cp gitea /usr/local/bin/gitea
|
||||||
|
|
||||||
See how to create [Linux service]({{< relref "run-as-service-in-ubuntu.en-us.md" >}})
|
See how to create [Linux service]({{< relref "run-as-service-in-ubuntu.en-us.md" >}})
|
||||||
|
|
||||||
|
## Updating to a new version
|
||||||
|
|
||||||
|
You can update to a new version of gitea by stopping gitea, replacing the binary at `/usr/local/bin/gitea` and restarting the instance.
|
||||||
|
The binary file name should not be changed during the update to avoid problems
|
||||||
|
in existing repositories.
|
||||||
|
|
||||||
|
It is recommended you do a [backup]({{< relref "doc/usage/backup-and-restore.en-us.md" >}}) before updating your installation.
|
||||||
|
|
||||||
|
If you have carried out the installation steps as described above, the binary should
|
||||||
|
have the generic name `gitea`. Do not change this, i.e. to include the version number.
|
||||||
|
|
||||||
|
See below for troubleshooting instructions to repair broken repositories after
|
||||||
|
an update of your gitea version.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Old glibc versions
|
### Old glibc versions
|
||||||
|
@ -99,3 +121,25 @@ For errors like `702 runWeb()] [E] Failed to start server: listen tcp 0.0.0.0:30
|
||||||
bind: address already in use` gitea needs to be started on another free port. This
|
bind: address already in use` gitea needs to be started on another free port. This
|
||||||
is possible using `./gitea web -p $PORT`. It's possible another instance of gitea
|
is possible using `./gitea web -p $PORT`. It's possible another instance of gitea
|
||||||
is already running.
|
is already running.
|
||||||
|
|
||||||
|
### Git error after updating to a new version of gitea
|
||||||
|
|
||||||
|
If the binary file name has been changed during the update to a new version of gitea,
|
||||||
|
git hooks in existing repositories will not work any more. In that case, a git
|
||||||
|
error will be displayed when pushing to the repository.
|
||||||
|
|
||||||
|
```
|
||||||
|
remote: ./hooks/pre-receive.d/gitea: line 2: [...]: No such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
The `[...]` part of the error message will contain the path to your previous gitea
|
||||||
|
binary.
|
||||||
|
|
||||||
|
To solve this, go to the admin options and run the task `Resynchronize pre-receive,
|
||||||
|
update and post-receive hooks of all repositories` to update all hooks to contain
|
||||||
|
the new binary path. Please note that this overwrite all git hooks including ones
|
||||||
|
with customizations made.
|
||||||
|
|
||||||
|
If you aren't using the built-in to Gitea ssh server you will also need to re-write
|
||||||
|
the authorized key file by running the `Update the '.ssh/authorized_keys' file with
|
||||||
|
Gitea SSH keys.` task in the admin options.
|
||||||
|
|
|
@ -64,3 +64,16 @@ bundled templates, options, plugins and themes are in `/usr/local/share/gitea`,
|
||||||
is in `/usr/local/etc/rc.d/gitea`.
|
is in `/usr/local/etc/rc.d/gitea`.
|
||||||
|
|
||||||
To enable Gitea to run as a service, run `sysrc gitea_enable=YES` and start it with `service gitea start`.
|
To enable Gitea to run as a service, run `sysrc gitea_enable=YES` and start it with `service gitea start`.
|
||||||
|
|
||||||
|
## Cloudron
|
||||||
|
|
||||||
|
Gitea is available as a 1-click install on [Cloudron](https://cloudron.io). For those unaware,
|
||||||
|
Cloudron makes it easy to run apps like Gitea on your server and keep them up-to-date and secure.
|
||||||
|
|
||||||
|
[![Install](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=io.gitea.cloudronapp)
|
||||||
|
|
||||||
|
The Gitea package is maintained [here](https://git.cloudron.io/cloudron/gitea-app).
|
||||||
|
|
||||||
|
There is a [demo instance](https://my-demo.cloudron.me) (username: cloudron password: cloudron) where
|
||||||
|
you can experiment with running Gitea.
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,8 @@ image as a service. Since there is no database available one can be initialized
|
||||||
Create a directory like `gitea` and paste the following content into a file named `docker-compose.yml`.
|
Create a directory like `gitea` and paste the following content into a file named `docker-compose.yml`.
|
||||||
Note that the volume should be owned by the user/group with the UID/GID specified in the config file.
|
Note that the volume should be owned by the user/group with the UID/GID specified in the config file.
|
||||||
If you don't give the volume correct permissions, the container may not start.
|
If you don't give the volume correct permissions, the container may not start.
|
||||||
|
Also be aware that the tag `:latest` will install the current development version.
|
||||||
|
For a stable release you can use `:1` or specify a certain release like `:1.5.1`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "2"
|
version: "2"
|
||||||
|
@ -103,6 +105,11 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- USER_UID=1000
|
- USER_UID=1000
|
||||||
- USER_GID=1000
|
- USER_GID=1000
|
||||||
|
+ - DB_TYPE=mysql
|
||||||
|
+ - DB_HOST=db:3306
|
||||||
|
+ - DB_NAME=gitea
|
||||||
|
+ - DB_USER=gitea
|
||||||
|
+ - DB_PASSWD=gitea
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- gitea
|
- gitea
|
||||||
|
@ -146,6 +153,11 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- USER_UID=1000
|
- USER_UID=1000
|
||||||
- USER_GID=1000
|
- USER_GID=1000
|
||||||
|
+ - DB_TYPE=postgres
|
||||||
|
+ - DB_HOST=db:5432
|
||||||
|
+ - DB_NAME=gitea
|
||||||
|
+ - DB_USER=gitea
|
||||||
|
+ - DB_PASSWD=gitea
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- gitea
|
- gitea
|
||||||
|
|
|
@ -33,50 +33,53 @@ There are some basic steps to follow. On a Linux system run as the Gogs user:
|
||||||
* Enter Gitea admin panel on the UI, run `Rewrite '.ssh/authorized_keys' file`.
|
* Enter Gitea admin panel on the UI, run `Rewrite '.ssh/authorized_keys' file`.
|
||||||
* If custom or config path was changed, run `Rewrite all update hook of repositories`.
|
* If custom or config path was changed, run `Rewrite all update hook of repositories`.
|
||||||
|
|
||||||
### Change gogs specific information:
|
## Change gogs specific information
|
||||||
|
|
||||||
* Rename `gogs-repositories/` to `gitea-repositories/`
|
* Rename `gogs-repositories/` to `gitea-repositories/`
|
||||||
* Rename `gogs-data/` to `gitea-data/`
|
* Rename `gogs-data/` to `gitea-data/`
|
||||||
* In `gitea/custom/conf/app.ini` change:
|
* In `gitea/custom/conf/app.ini` change:
|
||||||
|
|
||||||
### Upgrading to most recent `gitea` version:
|
FROM:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[database]
|
||||||
|
PATH = /home/:USER/gogs/data/:DATABASE.db
|
||||||
|
[attachment]
|
||||||
|
PATH = /home/:USER/gogs-data/attachments
|
||||||
|
[picture]
|
||||||
|
AVATAR_UPLOAD_PATH = /home/:USER/gogs-data/avatars
|
||||||
|
[log]
|
||||||
|
ROOT_PATH = /home/:USER/gogs/log
|
||||||
|
```
|
||||||
|
|
||||||
|
TO:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[database]
|
||||||
|
PATH = /home/:USER/gitea/data/:DATABASE.db
|
||||||
|
[attachment]
|
||||||
|
PATH = /home/:USER/gitea-data/attachments
|
||||||
|
[picture]
|
||||||
|
AVATAR_UPLOAD_PATH = /home/:USER/gitea-data/avatars
|
||||||
|
[log]
|
||||||
|
ROOT_PATH = /home/:USER/gitea/log
|
||||||
|
```
|
||||||
|
|
||||||
|
* Verify by starting Gitea with `gitea web`
|
||||||
|
|
||||||
|
## Upgrading to most recent `gitea` version
|
||||||
|
|
||||||
After successful migration from `gogs` to `gitea 1.0.x` it is possible to upgrade to the recent `gitea` version.
|
After successful migration from `gogs` to `gitea 1.0.x` it is possible to upgrade to the recent `gitea` version.
|
||||||
Simply download the file matching the destination platform from the [downloads page](https://dl.gitea.io/gitea)
|
Simply download the file matching the destination platform from the [downloads page](https://dl.gitea.io/gitea)
|
||||||
and replace the binary.
|
and replace the binary.
|
||||||
|
|
||||||
FROM:
|
## Troubleshooting
|
||||||
```
|
|
||||||
[database]
|
|
||||||
PATH = /home/:USER/gogs/data/:DATABASE.db
|
|
||||||
[attachment]
|
|
||||||
PATH = /home/:USER/gogs-data/attachments
|
|
||||||
[picture]
|
|
||||||
AVATAR_UPLOAD_PATH = /home/:USER/gogs-data/avatars
|
|
||||||
[log]
|
|
||||||
ROOT_PATH = /home/:USER/gogs/log
|
|
||||||
```
|
|
||||||
|
|
||||||
TO:
|
|
||||||
```
|
|
||||||
[database]
|
|
||||||
PATH = /home/:USER/gitea/data/:DATABASE.db
|
|
||||||
[attachment]
|
|
||||||
PATH = /home/:USER/gitea-data/attachments
|
|
||||||
[picture]
|
|
||||||
AVATAR_UPLOAD_PATH = /home/:USER/gitea-data/avatars
|
|
||||||
[log]
|
|
||||||
ROOT_PATH = /home/:USER/gitea/log
|
|
||||||
```
|
|
||||||
|
|
||||||
* Verify by starting Gitea with `gitea web`
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
* If errors are encountered relating to custom templates in the `gitea/custom/templates`
|
* If errors are encountered relating to custom templates in the `gitea/custom/templates`
|
||||||
folder, try moving the templates causing the errors away one by one. They may not be
|
folder, try moving the templates causing the errors away one by one. They may not be
|
||||||
compatible with Gitea or an update.
|
compatible with Gitea or an update.
|
||||||
|
|
||||||
### Add Gitea to startup on Unix
|
## Add Gitea to startup on Unix
|
||||||
|
|
||||||
Update the appropriate file from [gitea/contrib](https://github.com/go-gitea/gitea/tree/master/contrib)
|
Update the appropriate file from [gitea/contrib](https://github.com/go-gitea/gitea/tree/master/contrib)
|
||||||
with the right environment variables.
|
with the right environment variables.
|
||||||
|
|
|
@ -29,43 +29,45 @@ Veuillez suivre les étapes ci-dessous. Sur Unix, toute les commandes s'exécute
|
||||||
* Vérifiez votre installation en exécutant Gitea avec la commande `gitea web`.
|
* Vérifiez votre installation en exécutant Gitea avec la commande `gitea web`.
|
||||||
* Connectez vous au panel d'administration de Gitea et exécutez l'action `Rewrite '.ssh/authorized_keys' file`, puis l'action `Rewrite all update hook of repositories` (obligatoire si le chemin menant à votre configuration personnalisée à changé).
|
* Connectez vous au panel d'administration de Gitea et exécutez l'action `Rewrite '.ssh/authorized_keys' file`, puis l'action `Rewrite all update hook of repositories` (obligatoire si le chemin menant à votre configuration personnalisée à changé).
|
||||||
|
|
||||||
### Modifier les informations spécifiques de gogs
|
## Modifier les informations spécifiques de gogs
|
||||||
|
|
||||||
* Renommez `gogs-repositories/` vers `gitea-repositories/`
|
* Renommez `gogs-repositories/` vers `gitea-repositories/`
|
||||||
* Renommez `gogs-data/` to `gitea-data/`
|
* Renommez `gogs-data/` to `gitea-data/`
|
||||||
* Dans votre fichier `gitea/custom/conf/app.ini`, modifiez les éléments suivants:
|
* Dans votre fichier `gitea/custom/conf/app.ini`, modifiez les éléments suivants:
|
||||||
|
|
||||||
DE :
|
DE :
|
||||||
```
|
|
||||||
[database]
|
|
||||||
PATH = /home/:USER/gogs/data/:DATABASE.db
|
|
||||||
[attachment]
|
|
||||||
PATH = /home/:USER/gogs-data/attachments
|
|
||||||
[picture]
|
|
||||||
AVATAR_UPLOAD_PATH = /home/:USER/gogs-data/avatars
|
|
||||||
[log]
|
|
||||||
ROOT_PATH = /home/:USER/gogs/log
|
|
||||||
```
|
|
||||||
|
|
||||||
VERS :
|
```ini
|
||||||
```
|
[database]
|
||||||
[database]
|
PATH = /home/:USER/gogs/data/:DATABASE.db
|
||||||
PATH = /home/:USER/gitea/data/:DATABASE.db
|
[attachment]
|
||||||
[attachment]
|
PATH = /home/:USER/gogs-data/attachments
|
||||||
PATH = /home/:USER/gitea-data/attachments
|
[picture]
|
||||||
[picture]
|
AVATAR_UPLOAD_PATH = /home/:USER/gogs-data/avatars
|
||||||
AVATAR_UPLOAD_PATH = /home/:USER/gitea-data/avatars
|
[log]
|
||||||
[log]
|
ROOT_PATH = /home/:USER/gogs/log
|
||||||
ROOT_PATH = /home/:USER/gitea/log
|
```
|
||||||
```
|
|
||||||
|
VERS :
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[database]
|
||||||
|
PATH = /home/:USER/gitea/data/:DATABASE.db
|
||||||
|
[attachment]
|
||||||
|
PATH = /home/:USER/gitea-data/attachments
|
||||||
|
[picture]
|
||||||
|
AVATAR_UPLOAD_PATH = /home/:USER/gitea-data/avatars
|
||||||
|
[log]
|
||||||
|
ROOT_PATH = /home/:USER/gitea/log
|
||||||
|
```
|
||||||
|
|
||||||
* Vérifiez votre installation en exécutant Gitea avec la commande `gitea web`.
|
* Vérifiez votre installation en exécutant Gitea avec la commande `gitea web`.
|
||||||
|
|
||||||
### Dépannage
|
## Dépannage
|
||||||
|
|
||||||
* Si vous rencontrez des erreurs relatives à des modèles personnalisés dans le dossier `gitea/custom/templates`, essayez de déplacer un par un les modèles provoquant les erreurs. Il est possible qu'ils ne soient pas compatibles avec Gitea.
|
* Si vous rencontrez des erreurs relatives à des modèles personnalisés dans le dossier `gitea/custom/templates`, essayez de déplacer un par un les modèles provoquant les erreurs. Il est possible qu'ils ne soient pas compatibles avec Gitea.
|
||||||
|
|
||||||
### Démarrer automatiquement Gitea (Unix)
|
## Démarrer automatiquement Gitea (Unix)
|
||||||
|
|
||||||
Distributions utilisant systemd:
|
Distributions utilisant systemd:
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,50 @@ Admin operations:
|
||||||
- Examples:
|
- Examples:
|
||||||
- `gitea admin regenerate hooks`
|
- `gitea admin regenerate hooks`
|
||||||
- `gitea admin regenerate keys`
|
- `gitea admin regenerate keys`
|
||||||
|
- `auth`:
|
||||||
|
- `list`:
|
||||||
|
- Description: lists all external authentication sources that exist
|
||||||
|
- Options:
|
||||||
|
- `--config path`: Gitea configuration file path. Optional. (default: custom/conf/app.ini).
|
||||||
|
- Examples:
|
||||||
|
- `gitea auth list`
|
||||||
|
- `delete`:
|
||||||
|
- Options:
|
||||||
|
- `--id`: ID of source to be deleted. Required.
|
||||||
|
- `--config path`: Gitea configuration file path. Optional. (default: custom/conf/app.ini).
|
||||||
|
- Examples:
|
||||||
|
- `gitea auth delete --id 1`
|
||||||
|
- `add-oauth`:
|
||||||
|
- Options:
|
||||||
|
- `--config path`: Gitea configuration file path. Optional. (default: custom/conf/app.ini).
|
||||||
|
- `--name`: Application Name.
|
||||||
|
- `--provider`: OAuth2 Provider.
|
||||||
|
- `--key`: Client ID (Key).
|
||||||
|
- `--secret`: Client Secret.
|
||||||
|
- `--auto-discover-url`: OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider).
|
||||||
|
- `--use-custom-urls`: Use custom URLs for GitLab/GitHub OAuth endpoints.
|
||||||
|
- `--custom-auth-url`: Use a custom Authorization URL (option for GitLab/GitHub).
|
||||||
|
- `--custom-token-url`: Use a custom Token URL (option for GitLab/GitHub).
|
||||||
|
- `--custom-profile-url`: Use a custom Profile URL (option for GitLab/GitHub).
|
||||||
|
- `--custom-email-url`: Use a custom Email URL (option for GitHub).
|
||||||
|
- Examples:
|
||||||
|
- `gitea auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE`
|
||||||
|
- `update-oauth`:
|
||||||
|
- Options:
|
||||||
|
- `--id`: ID of source to be updated. Required.
|
||||||
|
- `--config path`: Gitea configuration file path. Optional. (default: custom/conf/app.ini).
|
||||||
|
- `--name`: Application Name.
|
||||||
|
- `--provider`: OAuth2 Provider.
|
||||||
|
- `--key`: Client ID (Key).
|
||||||
|
- `--secret`: Client Secret.
|
||||||
|
- `--auto-discover-url`: OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider).
|
||||||
|
- `--use-custom-urls`: Use custom URLs for GitLab/GitHub OAuth endpoints.
|
||||||
|
- `--custom-auth-url`: Use a custom Authorization URL (option for GitLab/GitHub).
|
||||||
|
- `--custom-token-url`: Use a custom Token URL (option for GitLab/GitHub).
|
||||||
|
- `--custom-profile-url`: Use a custom Profile URL (option for GitLab/GitHub).
|
||||||
|
- `--custom-email-url`: Use a custom Email URL (option for GitHub).
|
||||||
|
- Examples:
|
||||||
|
- `gitea auth update-oauth --id 1 --name external-github-updated`
|
||||||
|
|
||||||
#### cert
|
#### cert
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,24 @@ KEY_FILE = key.pem
|
||||||
```
|
```
|
||||||
To learn more about the config values, please checkout the [Config Cheat Sheet](../config-cheat-sheet#server).
|
To learn more about the config values, please checkout the [Config Cheat Sheet](../config-cheat-sheet#server).
|
||||||
|
|
||||||
|
## Using Let's Encrypt
|
||||||
|
|
||||||
|
[Let's Encrypt](https://letsencrypt.org/) is a Certificate Authority that allows you to automatically request and renew SSL/TLS certificates. In addition to starting Gitea on your configured port, to request HTTPS certificates Gitea will also need to listed on port 80, and will set up an autoredirect to HTTPS for you. Let's Encrypt will need to be able to access Gitea via the Internet to verify your ownership of the domain.
|
||||||
|
|
||||||
|
By using Lets Encrypt **you must consent** to their [terms of service](https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[server]
|
||||||
|
PROTOCOL=https
|
||||||
|
DOMAIN=git.example.com
|
||||||
|
ENABLE_LETSENCRYPT=true
|
||||||
|
LETSENCRYPT_ACCEPTTOS=true
|
||||||
|
LETSENCRYPT_DIRECTORY=https
|
||||||
|
LETSENCRYPT_EMAIL=email@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
To learn more about the config values, please checkout the [Config Cheat Sheet](../config-cheat-sheet#server).
|
||||||
|
|
||||||
## Using reverse proxy
|
## Using reverse proxy
|
||||||
|
|
||||||
Setup up your reverse proxy like shown in the [reverse proxy guide](../reverse-proxies).
|
Setup up your reverse proxy like shown in the [reverse proxy guide](../reverse-proxies).
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
date: "2018-06-01T19:00:00+02:00"
|
||||||
|
title: "Usage: Pull Request"
|
||||||
|
slug: "pull-request"
|
||||||
|
weight: 13
|
||||||
|
toc: true
|
||||||
|
draft: false
|
||||||
|
menu:
|
||||||
|
sidebar:
|
||||||
|
parent: "usage"
|
||||||
|
name: "Pull Request"
|
||||||
|
weight: 13
|
||||||
|
identifier: "pull-request"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pull Request
|
||||||
|
|
||||||
|
## "Work In Progress" pull requests
|
||||||
|
|
||||||
|
Marking a pull request as being a work in progress will prevent that pull request from being accidentally merged. To mark a pull request as being a work in progress, you must prefix its title by `WIP:` or `[WIP]` (case insensitive). Those values are configurable in your `app.ini` file :
|
||||||
|
|
||||||
|
```
|
||||||
|
[repository.pull-request]
|
||||||
|
WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
|
||||||
|
```
|
||||||
|
|
||||||
|
The first value of the list will be used in helpers.
|
||||||
|
|
||||||
|
## Pull Request Templates
|
||||||
|
|
||||||
|
You can find more information about pull request templates in the dedicated page : [Issue and Pull Request templates](../issue-pull-request-templates)
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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
|
|
@ -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!
|
|
@ -11,6 +11,8 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
api "code.gitea.io/sdk/gitea"
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
|
func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
|
||||||
|
@ -47,8 +49,8 @@ func TestAPIAdminDeleteMissingSSHKey(t *testing.T) {
|
||||||
prepareTestEnv(t)
|
prepareTestEnv(t)
|
||||||
// user1 is an admin user
|
// user1 is an admin user
|
||||||
session := loginUser(t, "user1")
|
session := loginUser(t, "user1")
|
||||||
token := getTokenForLoggedInUser(t, session)
|
|
||||||
|
|
||||||
|
token := getTokenForLoggedInUser(t, session)
|
||||||
req := NewRequestf(t, "DELETE", "/api/v1/admin/users/user1/keys/%d?token="+token, models.NonexistentID)
|
req := NewRequestf(t, "DELETE", "/api/v1/admin/users/user1/keys/%d?token="+token, models.NonexistentID)
|
||||||
session.MakeRequest(t, req, http.StatusNotFound)
|
session.MakeRequest(t, req, http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
@ -75,3 +77,32 @@ func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) {
|
||||||
adminUsername, newPublicKey.ID)
|
adminUsername, newPublicKey.ID)
|
||||||
session.MakeRequest(t, req, http.StatusForbidden)
|
session.MakeRequest(t, req, http.StatusForbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPISudoUser(t *testing.T) {
|
||||||
|
prepareTestEnv(t)
|
||||||
|
adminUsername := "user1"
|
||||||
|
normalUsername := "user2"
|
||||||
|
session := loginUser(t, adminUsername)
|
||||||
|
token := getTokenForLoggedInUser(t, session)
|
||||||
|
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/user?sudo=%s&token=%s", normalUsername, token)
|
||||||
|
req := NewRequest(t, "GET", urlStr)
|
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
var user api.User
|
||||||
|
DecodeJSON(t, resp, &user)
|
||||||
|
|
||||||
|
assert.Equal(t, normalUsername, user.UserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPISudoUserForbidden(t *testing.T) {
|
||||||
|
prepareTestEnv(t)
|
||||||
|
adminUsername := "user1"
|
||||||
|
normalUsername := "user2"
|
||||||
|
|
||||||
|
session := loginUser(t, normalUsername)
|
||||||
|
token := getTokenForLoggedInUser(t, session)
|
||||||
|
|
||||||
|
urlStr := fmt.Sprintf("/api/v1/user?sudo=%s&token=%s", adminUsername, token)
|
||||||
|
req := NewRequest(t, "GET", urlStr)
|
||||||
|
session.MakeRequest(t, req, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
package integrations
|
package integrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
api "code.gitea.io/sdk/gitea"
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
"fmt"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,13 @@
|
||||||
package integrations
|
package integrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/auth"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/sdk/gitea"
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -29,3 +32,27 @@ func TestAPIViewPulls(t *testing.T) {
|
||||||
expectedLen := models.GetCount(t, &models.Issue{RepoID: repo.ID}, models.Cond("is_pull = ?", true))
|
expectedLen := models.GetCount(t, &models.Issue{RepoID: repo.ID}, models.Cond("is_pull = ?", true))
|
||||||
assert.Len(t, pulls, expectedLen)
|
assert.Len(t, pulls, expectedLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAPIMergePullWIP ensures that we can't merge a WIP pull request
|
||||||
|
func TestAPIMergePullWIP(t *testing.T) {
|
||||||
|
prepareTestEnv(t)
|
||||||
|
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
||||||
|
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
|
||||||
|
pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{Status: models.PullRequestStatusMergeable}, models.Cond("has_merged = ?", false)).(*models.PullRequest)
|
||||||
|
pr.LoadIssue()
|
||||||
|
pr.Issue.ChangeTitle(owner, setting.Repository.PullRequest.WorkInProgressPrefixes[0]+" "+pr.Issue.Title)
|
||||||
|
|
||||||
|
// force reload
|
||||||
|
pr.LoadAttributes()
|
||||||
|
|
||||||
|
assert.Contains(t, pr.Issue.Title, setting.Repository.PullRequest.WorkInProgressPrefixes[0])
|
||||||
|
|
||||||
|
session := loginUser(t, owner.Name)
|
||||||
|
token := getTokenForLoggedInUser(t, session)
|
||||||
|
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", owner.Name, repo.Name, pr.Index, token), &auth.MergePullRequestForm{
|
||||||
|
MergeMessageField: pr.Issue.Title,
|
||||||
|
Do: string(models.MergeStyleMerge),
|
||||||
|
})
|
||||||
|
|
||||||
|
session.MakeRequest(t, req, http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
|
|
@ -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: 17},
|
nil: {count: 19},
|
||||||
user: {count: 17},
|
user: {count: 19},
|
||||||
user2: {count: 17}},
|
user2: {count: 19}},
|
||||||
},
|
},
|
||||||
{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},
|
||||||
|
@ -147,8 +147,8 @@ func TestAPISearchRepo(t *testing.T) {
|
||||||
if userToLogin != nil && userToLogin.ID > 0 {
|
if userToLogin != nil && userToLogin.ID > 0 {
|
||||||
testName = fmt.Sprintf("LoggedUser%d", userToLogin.ID)
|
testName = fmt.Sprintf("LoggedUser%d", userToLogin.ID)
|
||||||
session = loginUser(t, userToLogin.Name)
|
session = loginUser(t, userToLogin.Name)
|
||||||
userID = userToLogin.ID
|
|
||||||
token = getTokenForLoggedInUser(t, session)
|
token = getTokenForLoggedInUser(t, session)
|
||||||
|
userID = userToLogin.ID
|
||||||
} else {
|
} else {
|
||||||
testName = "AnonymousUser"
|
testName = "AnonymousUser"
|
||||||
session = emptyTestSession(t)
|
session = emptyTestSession(t)
|
||||||
|
@ -216,7 +216,6 @@ func TestAPIOrgRepos(t *testing.T) {
|
||||||
sourceOrg := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)
|
sourceOrg := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)
|
||||||
// Login as User2.
|
// Login as User2.
|
||||||
session := loginUser(t, user.Name)
|
session := loginUser(t, user.Name)
|
||||||
|
|
||||||
token := getTokenForLoggedInUser(t, session)
|
token := getTokenForLoggedInUser(t, session)
|
||||||
req := NewRequestf(t, "GET", "/api/v1/orgs/%s/repos?token="+token, sourceOrg.Name)
|
req := NewRequestf(t, "GET", "/api/v1/orgs/%s/repos?token="+token, sourceOrg.Name)
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
@ -266,3 +265,26 @@ func TestAPIRepoMigrate(t *testing.T) {
|
||||||
session.MakeRequest(t, req, testCase.expectedStatus)
|
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)
|
||||||
|
token := getTokenForLoggedInUser(t, session)
|
||||||
|
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos?token="+token, testCase.orgName), &api.CreateRepoOption{
|
||||||
|
Name: testCase.repoName,
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, testCase.expectedStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -272,6 +272,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 {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
|
"github.com/Unknwon/i18n"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -123,3 +124,23 @@ func TestPullCleanUpAfterMerge(t *testing.T) {
|
||||||
|
|
||||||
assert.EqualValues(t, "Branch 'user1/feature/test' has been deleted.", resultMsg)
|
assert.EqualValues(t, "Branch 'user1/feature/test' has been deleted.", resultMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCantMergeWorkInProgress(t *testing.T) {
|
||||||
|
prepareTestEnv(t)
|
||||||
|
session := loginUser(t, "user1")
|
||||||
|
testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
|
||||||
|
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
||||||
|
|
||||||
|
resp := testPullCreate(t, session, "user1", "repo1", "master", "[wip] This is a pull title")
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", resp.Header().Get("Location"))
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
text := strings.TrimSpace(htmlDoc.doc.Find(".merge.segment > .text.grey").Text())
|
||||||
|
assert.NotEmpty(t, text, "Can't find WIP text")
|
||||||
|
|
||||||
|
// remove <strong /> from lang
|
||||||
|
expected := i18n.Tr("en", "repo.pulls.cannot_merge_work_in_progress", "[wip]")
|
||||||
|
replacer := strings.NewReplacer("<strong>", "", "</strong>", "")
|
||||||
|
assert.Equal(t, replacer.Replace(expected), text, "Unable to find WIP text")
|
||||||
|
}
|
||||||
|
|
1
main.go
1
main.go
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,9 @@ const (
|
||||||
ActionReopenPullRequest // 15
|
ActionReopenPullRequest // 15
|
||||||
ActionDeleteTag // 16
|
ActionDeleteTag // 16
|
||||||
ActionDeleteBranch // 17
|
ActionDeleteBranch // 17
|
||||||
|
ActionMirrorSyncPush // 18
|
||||||
|
ActionMirrorSyncCreate // 19
|
||||||
|
ActionMirrorSyncDelete // 20
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -122,6 +125,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 +480,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -728,6 +741,71 @@ func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error
|
||||||
return mergePullRequestAction(x, actUser, repo, pull)
|
return mergePullRequestAction(x, actUser, repo, pull)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mirrorSyncAction(e Engine, opType ActionType, repo *Repository, refName string, data []byte) error {
|
||||||
|
if err := notifyWatchers(e, &Action{
|
||||||
|
ActUserID: repo.OwnerID,
|
||||||
|
ActUser: repo.MustOwner(),
|
||||||
|
OpType: opType,
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Repo: repo,
|
||||||
|
IsPrivate: repo.IsPrivate,
|
||||||
|
RefName: refName,
|
||||||
|
Content: string(data),
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("notifyWatchers: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MirrorSyncPushActionOptions mirror synchronization action options.
|
||||||
|
type MirrorSyncPushActionOptions struct {
|
||||||
|
RefName string
|
||||||
|
OldCommitID string
|
||||||
|
NewCommitID string
|
||||||
|
Commits *PushCommits
|
||||||
|
}
|
||||||
|
|
||||||
|
// MirrorSyncPushAction adds new action for mirror synchronization of pushed commits.
|
||||||
|
func MirrorSyncPushAction(repo *Repository, opts MirrorSyncPushActionOptions) error {
|
||||||
|
if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
|
||||||
|
opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
|
||||||
|
}
|
||||||
|
|
||||||
|
apiCommits := opts.Commits.ToAPIPayloadCommits(repo.HTMLURL())
|
||||||
|
|
||||||
|
opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
|
||||||
|
apiPusher := repo.MustOwner().APIFormat()
|
||||||
|
if err := PrepareWebhooks(repo, HookEventPush, &api.PushPayload{
|
||||||
|
Ref: opts.RefName,
|
||||||
|
Before: opts.OldCommitID,
|
||||||
|
After: opts.NewCommitID,
|
||||||
|
CompareURL: setting.AppURL + opts.Commits.CompareURL,
|
||||||
|
Commits: apiCommits,
|
||||||
|
Repo: repo.APIFormat(AccessModeOwner),
|
||||||
|
Pusher: apiPusher,
|
||||||
|
Sender: apiPusher,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("PrepareWebhooks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(opts.Commits)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mirrorSyncAction(x, ActionMirrorSyncPush, repo, opts.RefName, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MirrorSyncCreateAction adds new action for mirror synchronization of new reference.
|
||||||
|
func MirrorSyncCreateAction(repo *Repository, refName string) error {
|
||||||
|
return mirrorSyncAction(x, ActionMirrorSyncCreate, repo, refName, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MirrorSyncDeleteAction adds new action for mirror synchronization of delete reference.
|
||||||
|
func MirrorSyncDeleteAction(repo *Repository, refName string) error {
|
||||||
|
return mirrorSyncAction(x, ActionMirrorSyncDelete, repo, refName, nil)
|
||||||
|
}
|
||||||
|
|
||||||
// GetFeedsOptions options for retrieving feeds
|
// GetFeedsOptions options for retrieving feeds
|
||||||
type GetFeedsOptions struct {
|
type GetFeedsOptions struct {
|
||||||
RequestedUser *User
|
RequestedUser *User
|
||||||
|
|
107
models/error.go
107
models/error.go
|
@ -1259,3 +1259,110 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// __________ .__
|
||||||
|
// \______ \ _______ _|__| ______ _ __
|
||||||
|
// | _// __ \ \/ / |/ __ \ \/ \/ /
|
||||||
|
// | | \ ___/\ /| \ ___/\ /
|
||||||
|
// |____|_ /\___ >\_/ |__|\___ >\/\_/
|
||||||
|
// \/ \/ \/
|
||||||
|
|
||||||
|
// ErrReviewNotExist represents a "ReviewNotExist" kind of error.
|
||||||
|
type ErrReviewNotExist struct {
|
||||||
|
ID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrReviewNotExist checks if an error is a ErrReviewNotExist.
|
||||||
|
func IsErrReviewNotExist(err error) bool {
|
||||||
|
_, ok := err.(ErrReviewNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrReviewNotExist) Error() string {
|
||||||
|
return fmt.Sprintf("review does not exist [id: %d]", err.ID)
|
||||||
|
}
|
||||||
|
|
|
@ -20,3 +20,35 @@
|
||||||
issue_id: 1 # in repo_id 1
|
issue_id: 1 # in repo_id 1
|
||||||
content: "meh..."
|
content: "meh..."
|
||||||
created_unix: 946684812
|
created_unix: 946684812
|
||||||
|
-
|
||||||
|
id: 4
|
||||||
|
type: 21 # code comment
|
||||||
|
poster_id: 1
|
||||||
|
issue_id: 2
|
||||||
|
content: "meh..."
|
||||||
|
review_id: 4
|
||||||
|
line: 4
|
||||||
|
tree_path: "README.md"
|
||||||
|
created_unix: 946684812
|
||||||
|
invalidated: false
|
||||||
|
-
|
||||||
|
id: 5
|
||||||
|
type: 21 # code comment
|
||||||
|
poster_id: 1
|
||||||
|
issue_id: 2
|
||||||
|
content: "meh..."
|
||||||
|
line: -4
|
||||||
|
tree_path: "README.md"
|
||||||
|
created_unix: 946684812
|
||||||
|
invalidated: false
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 6
|
||||||
|
type: 21 # code comment
|
||||||
|
poster_id: 1
|
||||||
|
issue_id: 2
|
||||||
|
content: "it's already invalidated. boring..."
|
||||||
|
line: -4
|
||||||
|
tree_path: "README.md"
|
||||||
|
created_unix: 946684812
|
||||||
|
invalidated: true
|
||||||
|
|
|
@ -9,3 +9,11 @@
|
||||||
-
|
-
|
||||||
repo_id: 1
|
repo_id: 1
|
||||||
topic_id: 3
|
topic_id: 3
|
||||||
|
|
||||||
|
-
|
||||||
|
repo_id: 33
|
||||||
|
topic_id: 1
|
||||||
|
|
||||||
|
-
|
||||||
|
repo_id: 33
|
||||||
|
topic_id: 4
|
||||||
|
|
|
@ -407,3 +407,25 @@
|
||||||
lower_name: utf8
|
lower_name: utf8
|
||||||
name: utf8
|
name: utf8
|
||||||
is_private: false
|
is_private: false
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 34
|
||||||
|
owner_id: 21
|
||||||
|
lower_name: golang
|
||||||
|
name: golang
|
||||||
|
is_private: false
|
||||||
|
num_stars: 0
|
||||||
|
num_forks: 0
|
||||||
|
num_issues: 0
|
||||||
|
is_mirror: false
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 35
|
||||||
|
owner_id: 21
|
||||||
|
lower_name: graphql
|
||||||
|
name: graphql
|
||||||
|
is_private: false
|
||||||
|
num_stars: 0
|
||||||
|
num_forks: 0
|
||||||
|
num_issues: 0
|
||||||
|
is_mirror: false
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
-
|
||||||
|
id: 1
|
||||||
|
type: 1
|
||||||
|
reviewer_id: 1
|
||||||
|
issue_id: 2
|
||||||
|
content: "Demo Review"
|
||||||
|
updated_unix: 946684810
|
||||||
|
created_unix: 946684810
|
||||||
|
-
|
||||||
|
id: 2
|
||||||
|
type: 1
|
||||||
|
reviewer_id: 534543
|
||||||
|
issue_id: 534543
|
||||||
|
content: "Invalid Review #1"
|
||||||
|
updated_unix: 946684810
|
||||||
|
created_unix: 946684810
|
||||||
|
-
|
||||||
|
id: 3
|
||||||
|
type: 1
|
||||||
|
reviewer_id: 1
|
||||||
|
issue_id: 343545
|
||||||
|
content: "Invalid Review #2"
|
||||||
|
updated_unix: 946684810
|
||||||
|
created_unix: 946684810
|
||||||
|
-
|
||||||
|
id: 4
|
||||||
|
type: 0 # Pending review
|
||||||
|
reviewer_id: 1
|
||||||
|
issue_id: 2
|
||||||
|
content: "Pending Review"
|
||||||
|
updated_unix: 946684810
|
||||||
|
created_unix: 946684810
|
|
@ -1,7 +1,7 @@
|
||||||
-
|
-
|
||||||
id: 1
|
id: 1
|
||||||
name: golang
|
name: golang
|
||||||
repo_count: 1
|
repo_count: 2
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 2
|
id: 2
|
||||||
|
@ -11,3 +11,7 @@
|
||||||
- id: 3
|
- id: 3
|
||||||
name: SQL
|
name: SQL
|
||||||
repo_count: 1
|
repo_count: 1
|
||||||
|
|
||||||
|
- id: 4
|
||||||
|
name: graphql
|
||||||
|
repo_count: 1
|
||||||
|
|
|
@ -314,3 +314,18 @@
|
||||||
avatar_email: user20@example.com
|
avatar_email: user20@example.com
|
||||||
num_repos: 4
|
num_repos: 4
|
||||||
is_active: true
|
is_active: true
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 21
|
||||||
|
lower_name: user21
|
||||||
|
name: user21
|
||||||
|
full_name: User 21
|
||||||
|
email: user21@example.com
|
||||||
|
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
|
||||||
|
type: 0 # individual
|
||||||
|
salt: ZogKvWdyEx
|
||||||
|
is_admin: false
|
||||||
|
avatar: avatar21
|
||||||
|
avatar_email: user21@example.com
|
||||||
|
num_repos: 2
|
||||||
|
is_active: true
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -57,6 +59,7 @@ type DiffLine struct {
|
||||||
RightIdx int
|
RightIdx int
|
||||||
Type DiffLineType
|
Type DiffLineType
|
||||||
Content string
|
Content string
|
||||||
|
Comments []*Comment
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetType returns the type of a DiffLine.
|
// GetType returns the type of a DiffLine.
|
||||||
|
@ -64,6 +67,19 @@ func (d *DiffLine) GetType() int {
|
||||||
return int(d.Type)
|
return int(d.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanComment returns whether or not a line can get commented
|
||||||
|
func (d *DiffLine) CanComment() bool {
|
||||||
|
return len(d.Comments) == 0 && d.Type != DiffLineSection
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommentSide returns the comment side of the first comment, if not set returns empty string
|
||||||
|
func (d *DiffLine) GetCommentSide() string {
|
||||||
|
if len(d.Comments) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return d.Comments[0].DiffSide()
|
||||||
|
}
|
||||||
|
|
||||||
// DiffSection represents a section of a DiffFile.
|
// DiffSection represents a section of a DiffFile.
|
||||||
type DiffSection struct {
|
type DiffSection struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -225,11 +241,171 @@ type Diff struct {
|
||||||
IsIncomplete bool
|
IsIncomplete bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadComments loads comments into each line
|
||||||
|
func (diff *Diff) LoadComments(issue *Issue, currentUser *User) error {
|
||||||
|
allComments, err := FetchCodeComments(issue, currentUser)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, file := range diff.Files {
|
||||||
|
if lineCommits, ok := allComments[file.Name]; ok {
|
||||||
|
for _, section := range file.Sections {
|
||||||
|
for _, line := range section.Lines {
|
||||||
|
if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {
|
||||||
|
line.Comments = append(line.Comments, comments...)
|
||||||
|
}
|
||||||
|
if comments, ok := lineCommits[int64(line.RightIdx)]; ok {
|
||||||
|
line.Comments = append(line.Comments, comments...)
|
||||||
|
}
|
||||||
|
sort.SliceStable(line.Comments, func(i, j int) bool {
|
||||||
|
return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// NumFiles returns number of files changes in a diff.
|
// NumFiles returns number of files changes in a diff.
|
||||||
func (diff *Diff) NumFiles() int {
|
func (diff *Diff) NumFiles() int {
|
||||||
return len(diff.Files)
|
return len(diff.Files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
|
||||||
|
var hunkRegex = regexp.MustCompile(`^@@ -([0-9]+),([0-9]+) \+([0-9]+)(,([0-9]+))? @@`)
|
||||||
|
|
||||||
|
func isHeader(lof string) bool {
|
||||||
|
return strings.HasPrefix(lof, cmdDiffHead) || strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CutDiffAroundLine cuts a diff of a file in way that only the given line + numberOfLine above it will be shown
|
||||||
|
// it also recalculates hunks and adds the appropriate headers to the new diff.
|
||||||
|
// Warning: Only one-file diffs are allowed.
|
||||||
|
func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) string {
|
||||||
|
if line == 0 || numbersOfLine == 0 {
|
||||||
|
// no line or num of lines => no diff
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(originalDiff)
|
||||||
|
hunk := make([]string, 0)
|
||||||
|
// begin is the start of the hunk containing searched line
|
||||||
|
// end is the end of the hunk ...
|
||||||
|
// currentLine is the line number on the side of the searched line (differentiated by old)
|
||||||
|
// otherLine is the line number on the opposite side of the searched line (differentiated by old)
|
||||||
|
var begin, end, currentLine, otherLine int64
|
||||||
|
var headerLines int
|
||||||
|
for scanner.Scan() {
|
||||||
|
lof := scanner.Text()
|
||||||
|
// Add header to enable parsing
|
||||||
|
if isHeader(lof) {
|
||||||
|
hunk = append(hunk, lof)
|
||||||
|
headerLines++
|
||||||
|
}
|
||||||
|
if currentLine > line {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Detect "hunk" with contains commented lof
|
||||||
|
if strings.HasPrefix(lof, "@@") {
|
||||||
|
// Already got our hunk. End of hunk detected!
|
||||||
|
if len(hunk) > headerLines {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
groups := hunkRegex.FindStringSubmatch(lof)
|
||||||
|
if old {
|
||||||
|
begin = com.StrTo(groups[1]).MustInt64()
|
||||||
|
end = com.StrTo(groups[2]).MustInt64()
|
||||||
|
// init otherLine with begin of opposite side
|
||||||
|
otherLine = com.StrTo(groups[3]).MustInt64()
|
||||||
|
} else {
|
||||||
|
begin = com.StrTo(groups[3]).MustInt64()
|
||||||
|
if groups[5] != "" {
|
||||||
|
end = com.StrTo(groups[5]).MustInt64()
|
||||||
|
} else {
|
||||||
|
end = 0
|
||||||
|
}
|
||||||
|
// init otherLine with begin of opposite side
|
||||||
|
otherLine = com.StrTo(groups[1]).MustInt64()
|
||||||
|
}
|
||||||
|
end += begin // end is for real only the number of lines in hunk
|
||||||
|
// lof is between begin and end
|
||||||
|
if begin <= line && end >= line {
|
||||||
|
hunk = append(hunk, lof)
|
||||||
|
currentLine = begin
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if len(hunk) > headerLines {
|
||||||
|
hunk = append(hunk, lof)
|
||||||
|
// Count lines in context
|
||||||
|
switch lof[0] {
|
||||||
|
case '+':
|
||||||
|
if !old {
|
||||||
|
currentLine++
|
||||||
|
} else {
|
||||||
|
otherLine++
|
||||||
|
}
|
||||||
|
case '-':
|
||||||
|
if old {
|
||||||
|
currentLine++
|
||||||
|
} else {
|
||||||
|
otherLine++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
currentLine++
|
||||||
|
otherLine++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No hunk found
|
||||||
|
if currentLine == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// headerLines + hunkLine (1) = totalNonCodeLines
|
||||||
|
if len(hunk)-headerLines-1 <= numbersOfLine {
|
||||||
|
// No need to cut the hunk => return existing hunk
|
||||||
|
return strings.Join(hunk, "\n")
|
||||||
|
}
|
||||||
|
var oldBegin, oldNumOfLines, newBegin, newNumOfLines int64
|
||||||
|
if old {
|
||||||
|
oldBegin = currentLine
|
||||||
|
newBegin = otherLine
|
||||||
|
} else {
|
||||||
|
oldBegin = otherLine
|
||||||
|
newBegin = currentLine
|
||||||
|
}
|
||||||
|
// headers + hunk header
|
||||||
|
newHunk := make([]string, headerLines)
|
||||||
|
// transfer existing headers
|
||||||
|
for idx, lof := range hunk[:headerLines] {
|
||||||
|
newHunk[idx] = lof
|
||||||
|
}
|
||||||
|
// transfer last n lines
|
||||||
|
for _, lof := range hunk[len(hunk)-numbersOfLine-1:] {
|
||||||
|
newHunk = append(newHunk, lof)
|
||||||
|
}
|
||||||
|
// calculate newBegin, ... by counting lines
|
||||||
|
for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
|
||||||
|
switch hunk[i][0] {
|
||||||
|
case '+':
|
||||||
|
newBegin--
|
||||||
|
newNumOfLines++
|
||||||
|
case '-':
|
||||||
|
oldBegin--
|
||||||
|
oldNumOfLines++
|
||||||
|
default:
|
||||||
|
oldBegin--
|
||||||
|
newBegin--
|
||||||
|
newNumOfLines++
|
||||||
|
oldNumOfLines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// construct the new hunk header
|
||||||
|
newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
|
||||||
|
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
|
||||||
|
return strings.Join(newHunk, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
const cmdDiffHead = "diff --git "
|
const cmdDiffHead = "diff --git "
|
||||||
|
|
||||||
// ParsePatch builds a Diff object from a io.Reader and some
|
// ParsePatch builds a Diff object from a io.Reader and some
|
||||||
|
@ -307,7 +483,6 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D
|
||||||
if curFileLinesCount >= maxLines {
|
if curFileLinesCount >= maxLines {
|
||||||
curFile.IsIncomplete = true
|
curFile.IsIncomplete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case line[0] == ' ':
|
case line[0] == ' ':
|
||||||
diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
|
diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
|
||||||
|
@ -462,6 +637,13 @@ func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*D
|
||||||
// passing the empty string as beforeCommitID returns a diff from the
|
// passing the empty string as beforeCommitID returns a diff from the
|
||||||
// parent commit.
|
// parent commit.
|
||||||
func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
|
func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
|
||||||
|
return GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacters, maxFiles, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDiffRangeWithWhitespaceBehavior builds a Diff between two commits of a repository.
|
||||||
|
// Passing the empty string as beforeCommitID returns a diff from the parent commit.
|
||||||
|
// The whitespaceBehavior is either an empty string or a git flag
|
||||||
|
func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int, whitespaceBehavior string) (*Diff, error) {
|
||||||
gitRepo, err := git.OpenRepository(repoPath)
|
gitRepo, err := git.OpenRepository(repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -473,17 +655,21 @@ func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxL
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
// if "after" commit given
|
if len(beforeCommitID) == 0 && commit.ParentCount() == 0 {
|
||||||
if len(beforeCommitID) == 0 {
|
cmd = exec.Command("git", "show", afterCommitID)
|
||||||
// First commit of repository.
|
|
||||||
if commit.ParentCount() == 0 {
|
|
||||||
cmd = exec.Command("git", "show", afterCommitID)
|
|
||||||
} else {
|
|
||||||
c, _ := commit.Parent(0)
|
|
||||||
cmd = exec.Command("git", "diff", "-M", c.ID.String(), afterCommitID)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
cmd = exec.Command("git", "diff", "-M", beforeCommitID, afterCommitID)
|
actualBeforeCommitID := beforeCommitID
|
||||||
|
if len(actualBeforeCommitID) == 0 {
|
||||||
|
parentCommit, _ := commit.Parent(0)
|
||||||
|
actualBeforeCommitID = parentCommit.ID.String()
|
||||||
|
}
|
||||||
|
diffArgs := []string{"diff", "-M"}
|
||||||
|
if len(whitespaceBehavior) != 0 {
|
||||||
|
diffArgs = append(diffArgs, whitespaceBehavior)
|
||||||
|
}
|
||||||
|
diffArgs = append(diffArgs, actualBeforeCommitID)
|
||||||
|
diffArgs = append(diffArgs, afterCommitID)
|
||||||
|
cmd = exec.Command("git", diffArgs...)
|
||||||
}
|
}
|
||||||
cmd.Dir = repoPath
|
cmd.Dir = repoPath
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
@ -524,32 +710,46 @@ const (
|
||||||
// GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
|
// GetRawDiff dumps diff results of repository in given commit ID to io.Writer.
|
||||||
// TODO: move this function to gogits/git-module
|
// TODO: move this function to gogits/git-module
|
||||||
func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Writer) error {
|
func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Writer) error {
|
||||||
|
return GetRawDiffForFile(repoPath, "", commitID, diffType, "", writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRawDiffForFile dumps diff results of file in given commit ID to io.Writer.
|
||||||
|
// TODO: move this function to gogits/git-module
|
||||||
|
func GetRawDiffForFile(repoPath, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
|
||||||
repo, err := git.OpenRepository(repoPath)
|
repo, err := git.OpenRepository(repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("OpenRepository: %v", err)
|
return fmt.Errorf("OpenRepository: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
commit, err := repo.GetCommit(commitID)
|
commit, err := repo.GetCommit(endCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GetCommit: %v", err)
|
return fmt.Errorf("GetCommit: %v", err)
|
||||||
}
|
}
|
||||||
|
fileArgs := make([]string, 0)
|
||||||
|
if len(file) > 0 {
|
||||||
|
fileArgs = append(fileArgs, "--", file)
|
||||||
|
}
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
switch diffType {
|
switch diffType {
|
||||||
case RawDiffNormal:
|
case RawDiffNormal:
|
||||||
if commit.ParentCount() == 0 {
|
if len(startCommit) != 0 {
|
||||||
cmd = exec.Command("git", "show", commitID)
|
cmd = exec.Command("git", append([]string{"diff", "-M", startCommit, endCommit}, fileArgs...)...)
|
||||||
|
} else if commit.ParentCount() == 0 {
|
||||||
|
cmd = exec.Command("git", append([]string{"show", endCommit}, fileArgs...)...)
|
||||||
} else {
|
} else {
|
||||||
c, _ := commit.Parent(0)
|
c, _ := commit.Parent(0)
|
||||||
cmd = exec.Command("git", "diff", "-M", c.ID.String(), commitID)
|
cmd = exec.Command("git", append([]string{"diff", "-M", c.ID.String(), endCommit}, fileArgs...)...)
|
||||||
}
|
}
|
||||||
case RawDiffPatch:
|
case RawDiffPatch:
|
||||||
if commit.ParentCount() == 0 {
|
if len(startCommit) != 0 {
|
||||||
cmd = exec.Command("git", "format-patch", "--no-signature", "--stdout", "--root", commitID)
|
query := fmt.Sprintf("%s...%s", endCommit, startCommit)
|
||||||
|
cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", "--root", query}, fileArgs...)...)
|
||||||
|
} else if commit.ParentCount() == 0 {
|
||||||
|
cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", "--root", endCommit}, fileArgs...)...)
|
||||||
} else {
|
} else {
|
||||||
c, _ := commit.Parent(0)
|
c, _ := commit.Parent(0)
|
||||||
query := fmt.Sprintf("%s...%s", commitID, c.ID.String())
|
query := fmt.Sprintf("%s...%s", endCommit, c.ID.String())
|
||||||
cmd = exec.Command("git", "format-patch", "--no-signature", "--stdout", query)
|
cmd = exec.Command("git", append([]string{"format-patch", "--no-signature", "--stdout", query}, fileArgs...)...)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid diffType: %s", diffType)
|
return fmt.Errorf("invalid diffType: %s", diffType)
|
||||||
|
@ -560,7 +760,6 @@ func GetRawDiff(repoPath, commitID string, diffType RawDiffType, writer io.Write
|
||||||
cmd.Dir = repoPath
|
cmd.Dir = repoPath
|
||||||
cmd.Stdout = writer
|
cmd.Stdout = writer
|
||||||
cmd.Stderr = stderr
|
cmd.Stderr = stderr
|
||||||
|
|
||||||
if err = cmd.Run(); err != nil {
|
if err = cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("Run: %v - %s", err, stderr)
|
return fmt.Errorf("Run: %v - %s", err, stderr)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
|
func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
|
||||||
|
@ -34,3 +36,106 @@ func TestDiffToHTML(t *testing.T) {
|
||||||
{Type: dmp.DiffEqual, Text: " biz"},
|
{Type: dmp.DiffEqual, Text: " biz"},
|
||||||
}, DiffLineDel))
|
}, DiffLineDel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exampleDiff = `diff --git a/README.md b/README.md
|
||||||
|
--- a/README.md
|
||||||
|
+++ b/README.md
|
||||||
|
@@ -1,3 +1,6 @@
|
||||||
|
# gitea-github-migrator
|
||||||
|
+
|
||||||
|
+ Build Status
|
||||||
|
- Latest Release
|
||||||
|
Docker Pulls
|
||||||
|
+ cut off
|
||||||
|
+ cut off`
|
||||||
|
|
||||||
|
func TestCutDiffAroundLine(t *testing.T) {
|
||||||
|
result := CutDiffAroundLine(strings.NewReader(exampleDiff), 4, false, 3)
|
||||||
|
resultByLine := strings.Split(result, "\n")
|
||||||
|
assert.Len(t, resultByLine, 7)
|
||||||
|
// Check if headers got transferred
|
||||||
|
assert.Equal(t, "diff --git a/README.md b/README.md", resultByLine[0])
|
||||||
|
assert.Equal(t, "--- a/README.md", resultByLine[1])
|
||||||
|
assert.Equal(t, "+++ b/README.md", resultByLine[2])
|
||||||
|
// Check if hunk header is calculated correctly
|
||||||
|
assert.Equal(t, "@@ -2,2 +3,2 @@", resultByLine[3])
|
||||||
|
// Check if line got transferred
|
||||||
|
assert.Equal(t, "+ Build Status", resultByLine[4])
|
||||||
|
|
||||||
|
// Must be same result as before since old line 3 == new line 5
|
||||||
|
newResult := CutDiffAroundLine(strings.NewReader(exampleDiff), 3, true, 3)
|
||||||
|
assert.Equal(t, result, newResult, "Must be same result as before since old line 3 == new line 5")
|
||||||
|
|
||||||
|
newResult = CutDiffAroundLine(strings.NewReader(exampleDiff), 6, false, 300)
|
||||||
|
assert.Equal(t, exampleDiff, newResult)
|
||||||
|
|
||||||
|
emptyResult := CutDiffAroundLine(strings.NewReader(exampleDiff), 6, false, 0)
|
||||||
|
assert.Empty(t, emptyResult)
|
||||||
|
|
||||||
|
// Line is out of scope
|
||||||
|
emptyResult = CutDiffAroundLine(strings.NewReader(exampleDiff), 434, false, 0)
|
||||||
|
assert.Empty(t, emptyResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCutDiffAroundLine(b *testing.B) {
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
CutDiffAroundLine(strings.NewReader(exampleDiff), 3, true, 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleCutDiffAroundLine() {
|
||||||
|
const diff = `diff --git a/README.md b/README.md
|
||||||
|
--- a/README.md
|
||||||
|
+++ b/README.md
|
||||||
|
@@ -1,3 +1,6 @@
|
||||||
|
# gitea-github-migrator
|
||||||
|
+
|
||||||
|
+ Build Status
|
||||||
|
- Latest Release
|
||||||
|
Docker Pulls
|
||||||
|
+ cut off
|
||||||
|
+ cut off`
|
||||||
|
result := CutDiffAroundLine(strings.NewReader(diff), 4, false, 3)
|
||||||
|
println(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDefaultDiff() *Diff {
|
||||||
|
return &Diff{
|
||||||
|
Files: []*DiffFile{
|
||||||
|
{
|
||||||
|
Name: "README.md",
|
||||||
|
Sections: []*DiffSection{
|
||||||
|
{
|
||||||
|
Lines: []*DiffLine{
|
||||||
|
{
|
||||||
|
LeftIdx: 4,
|
||||||
|
RightIdx: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestDiff_LoadComments(t *testing.T) {
|
||||||
|
issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
|
||||||
|
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
|
||||||
|
diff := setupDefaultDiff()
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
assert.NoError(t, diff.LoadComments(issue, user))
|
||||||
|
assert.Len(t, diff.Files[0].Sections[0].Lines[0].Comments, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiffLine_CanComment(t *testing.T) {
|
||||||
|
assert.False(t, (&DiffLine{Type: DiffLineSection}).CanComment())
|
||||||
|
assert.False(t, (&DiffLine{Type: DiffLineAdd, Comments: []*Comment{{Content: "bla"}}}).CanComment())
|
||||||
|
assert.True(t, (&DiffLine{Type: DiffLineAdd}).CanComment())
|
||||||
|
assert.True(t, (&DiffLine{Type: DiffLineDel}).CanComment())
|
||||||
|
assert.True(t, (&DiffLine{Type: DiffLinePlain}).CanComment())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiffLine_GetCommentSide(t *testing.T) {
|
||||||
|
assert.Equal(t, "previous", (&DiffLine{Comments: []*Comment{{Line: -3}}}).GetCommentSide())
|
||||||
|
assert.Equal(t, "proposed", (&DiffLine{Comments: []*Comment{{Line: 3}}}).GetCommentSide())
|
||||||
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
// Copyright 2018 The Gitea Authors.
|
||||||
|
// 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.
|
||||||
|
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/git"
|
||||||
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
"github.com/go-xorm/builder"
|
"github.com/go-xorm/builder"
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
|
@ -66,6 +72,14 @@ const (
|
||||||
CommentTypeModifiedDeadline
|
CommentTypeModifiedDeadline
|
||||||
// Removed a due date
|
// Removed a due date
|
||||||
CommentTypeRemovedDeadline
|
CommentTypeRemovedDeadline
|
||||||
|
// Dependency added
|
||||||
|
CommentTypeAddDependency
|
||||||
|
//Dependency removed
|
||||||
|
CommentTypeRemoveDependency
|
||||||
|
// Comment a line of code
|
||||||
|
CommentTypeCode
|
||||||
|
// Reviews a pull request by giving general feedback
|
||||||
|
CommentTypeReview
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentTag defines comment tag type
|
// CommentTag defines comment tag type
|
||||||
|
@ -81,29 +95,35 @@ 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 // - previous line / + proposed line
|
||||||
|
TreePath string
|
||||||
Content string `xorm:"TEXT"`
|
Content string `xorm:"TEXT"`
|
||||||
RenderedContent string `xorm:"-"`
|
RenderedContent string `xorm:"-"`
|
||||||
|
|
||||||
|
// Path represents the 4 lines of code cemented by this comment
|
||||||
|
Patch string `xorm:"TEXT"`
|
||||||
|
|
||||||
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
|
||||||
|
@ -115,6 +135,10 @@ type Comment struct {
|
||||||
|
|
||||||
// For view issue page.
|
// For view issue page.
|
||||||
ShowTag CommentTag `xorm:"-"`
|
ShowTag CommentTag `xorm:"-"`
|
||||||
|
|
||||||
|
Review *Review `xorm:"-"`
|
||||||
|
ReviewID int64
|
||||||
|
Invalidated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadIssue loads issue from database
|
// LoadIssue loads issue from database
|
||||||
|
@ -165,6 +189,20 @@ func (c *Comment) HTMLURL() string {
|
||||||
log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
|
log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
if c.Type == CommentTypeCode {
|
||||||
|
if c.ReviewID == 0 {
|
||||||
|
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||||
|
}
|
||||||
|
if c.Review == nil {
|
||||||
|
if err := c.LoadReview(); err != nil {
|
||||||
|
log.Warn("LoadReview(%d): %v", c.ReviewID, err)
|
||||||
|
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Review.Type <= ReviewTypePending {
|
||||||
|
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||||
|
}
|
||||||
|
}
|
||||||
return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
|
return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,6 +319,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) {
|
||||||
|
@ -327,27 +374,115 @@ func (c *Comment) LoadReactions() error {
|
||||||
return c.loadReactions(x)
|
return c.loadReactions(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Comment) loadReview(e Engine) (err error) {
|
||||||
|
if c.Review, err = getReviewByID(e, c.ReviewID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadReview loads the associated review
|
||||||
|
func (c *Comment) LoadReview() error {
|
||||||
|
return c.loadReview(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Comment) checkInvalidation(e Engine, doer *User, repo *git.Repository, branch string) error {
|
||||||
|
// FIXME differentiate between previous and proposed line
|
||||||
|
commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() {
|
||||||
|
c.Invalidated = true
|
||||||
|
return UpdateComment(doer, c, "")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckInvalidation checks if the line of code comment got changed by another commit.
|
||||||
|
// If the line got changed the comment is going to be invalidated.
|
||||||
|
func (c *Comment) CheckInvalidation(repo *git.Repository, doer *User, branch string) error {
|
||||||
|
return c.checkInvalidation(x, doer, repo, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
|
||||||
|
func (c *Comment) DiffSide() string {
|
||||||
|
if c.Line < 0 {
|
||||||
|
return "previous"
|
||||||
|
}
|
||||||
|
return "proposed"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsignedLine returns the LOC of the code comment without + or -
|
||||||
|
func (c *Comment) UnsignedLine() uint64 {
|
||||||
|
if c.Line < 0 {
|
||||||
|
return uint64(c.Line * -1)
|
||||||
|
}
|
||||||
|
return uint64(c.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsDiff returns c.Patch as *Diff
|
||||||
|
func (c *Comment) AsDiff() (*Diff, error) {
|
||||||
|
diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
|
||||||
|
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(diff.Files) == 0 {
|
||||||
|
return nil, fmt.Errorf("no file found for comment ID: %d", c.ID)
|
||||||
|
}
|
||||||
|
secs := diff.Files[0].Sections
|
||||||
|
if len(secs) == 0 {
|
||||||
|
return nil, fmt.Errorf("no sections found for comment ID: %d", c.ID)
|
||||||
|
}
|
||||||
|
return diff, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustAsDiff executes AsDiff and logs the error instead of returning
|
||||||
|
func (c *Comment) MustAsDiff() *Diff {
|
||||||
|
diff, err := c.AsDiff()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("MustAsDiff: %v", err)
|
||||||
|
}
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
// CodeCommentURL returns the url to a comment in code
|
||||||
|
func (c *Comment) CodeCommentURL() string {
|
||||||
|
err := c.LoadIssue()
|
||||||
|
if err != nil { // Silently dropping errors :unamused:
|
||||||
|
log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||||
|
}
|
||||||
|
|
||||||
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
|
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
|
||||||
var LabelID int64
|
var LabelID int64
|
||||||
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,
|
||||||
|
TreePath: opts.TreePath,
|
||||||
|
ReviewID: opts.ReviewID,
|
||||||
|
Patch: opts.Patch,
|
||||||
}
|
}
|
||||||
if _, err = e.Insert(comment); err != nil {
|
if _, err = e.Insert(comment); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -357,6 +492,14 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = sendCreateCommentAction(e, opts, comment); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) {
|
||||||
// Compose comment action, could be plain comment, close or reopen issue/pull request.
|
// Compose comment action, could be plain comment, close or reopen issue/pull request.
|
||||||
// This object will be used to notify watchers in the end of function.
|
// This object will be used to notify watchers in the end of function.
|
||||||
act := &Action{
|
act := &Action{
|
||||||
|
@ -369,14 +512,25 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
CommentID: comment.ID,
|
CommentID: comment.ID,
|
||||||
IsPrivate: opts.Repo.IsPrivate,
|
IsPrivate: opts.Repo.IsPrivate,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check comment type.
|
// Check comment type.
|
||||||
switch opts.Type {
|
switch opts.Type {
|
||||||
|
case CommentTypeCode:
|
||||||
|
if comment.ReviewID != 0 {
|
||||||
|
if comment.Review == nil {
|
||||||
|
if err := comment.LoadReview(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if comment.Review.Type <= ReviewTypePending {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
case CommentTypeComment:
|
case CommentTypeComment:
|
||||||
act.OpType = ActionCommentIssue
|
act.OpType = ActionCommentIssue
|
||||||
|
|
||||||
if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
|
if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check attachments
|
// Check attachments
|
||||||
|
@ -387,7 +541,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
if IsErrAttachmentNotExist(err) {
|
if IsErrAttachmentNotExist(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
|
return fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
|
||||||
}
|
}
|
||||||
attachments = append(attachments, attach)
|
attachments = append(attachments, attach)
|
||||||
}
|
}
|
||||||
|
@ -397,7 +551,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
attachments[i].CommentID = comment.ID
|
attachments[i].CommentID = comment.ID
|
||||||
// No assign value could be 0, so ignore AllCols().
|
// No assign value could be 0, so ignore AllCols().
|
||||||
if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
|
if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
|
||||||
return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
|
return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,7 +567,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID)
|
_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
case CommentTypeClose:
|
case CommentTypeClose:
|
||||||
|
@ -428,15 +582,13 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID)
|
_, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the issue's updated_unix column
|
// update the issue's updated_unix column
|
||||||
if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil {
|
if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify watchers for whatever action comes in, ignore if no action type.
|
// Notify watchers for whatever action comes in, ignore if no action type.
|
||||||
if act.OpType > 0 {
|
if act.OpType > 0 {
|
||||||
if err = notifyWatchers(e, act); err != nil {
|
if err = notifyWatchers(e, act); err != nil {
|
||||||
|
@ -446,8 +598,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
|
||||||
log.Error(4, "MailParticipants: %v", err)
|
log.Error(4, "MailParticipants: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return comment, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
|
func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
|
||||||
|
@ -549,6 +700,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
|
||||||
|
@ -557,17 +741,21 @@ 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
|
Patch string
|
||||||
Attachments []string // UUIDs of attachments
|
LineNum int64
|
||||||
|
TreePath string
|
||||||
|
ReviewID int64
|
||||||
|
Content string
|
||||||
|
Attachments []string // UUIDs of attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateComment creates comment of issue or commit.
|
// CreateComment creates comment of issue or commit.
|
||||||
|
@ -622,6 +810,58 @@ func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content stri
|
||||||
return comment, nil
|
return comment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateCodeComment creates a plain code comment at the specified line / path
|
||||||
|
func CreateCodeComment(doer *User, repo *Repository, issue *Issue, content, treePath string, line, reviewID int64) (*Comment, error) {
|
||||||
|
var commitID, patch string
|
||||||
|
pr, err := GetPullRequestByIssueID(issue.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetPullRequestByIssueID: %v", err)
|
||||||
|
}
|
||||||
|
if err := pr.GetBaseRepo(); err != nil {
|
||||||
|
return nil, fmt.Errorf("GetHeadRepo: %v", err)
|
||||||
|
}
|
||||||
|
gitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("OpenRepository: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME validate treePath
|
||||||
|
// Get latest commit referencing the commented line
|
||||||
|
// No need for get commit for base branch changes
|
||||||
|
if line > 0 {
|
||||||
|
commit, err := gitRepo.LineBlame(pr.GetGitRefName(), gitRepo.Path, treePath, uint(line))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %v", pr.GetGitRefName(), gitRepo.Path, treePath, line, err)
|
||||||
|
}
|
||||||
|
commitID = commit.ID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only fetch diff if comment is review comment
|
||||||
|
if reviewID != 0 {
|
||||||
|
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
|
||||||
|
}
|
||||||
|
patchBuf := new(bytes.Buffer)
|
||||||
|
if err := GetRawDiffForFile(gitRepo.Path, pr.MergeBase, headCommitID, RawDiffNormal, treePath, patchBuf); err != nil {
|
||||||
|
return nil, fmt.Errorf("GetRawDiffForLine[%s, %s, %s, %s]: %v", err, gitRepo.Path, pr.MergeBase, headCommitID, treePath)
|
||||||
|
}
|
||||||
|
patch = CutDiffAroundLine(strings.NewReader(patchBuf.String()), int64((&Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
|
||||||
|
}
|
||||||
|
return CreateComment(&CreateCommentOptions{
|
||||||
|
Type: CommentTypeCode,
|
||||||
|
Doer: doer,
|
||||||
|
Repo: repo,
|
||||||
|
Issue: issue,
|
||||||
|
Content: content,
|
||||||
|
LineNum: line,
|
||||||
|
TreePath: treePath,
|
||||||
|
CommitSHA: commitID,
|
||||||
|
ReviewID: reviewID,
|
||||||
|
Patch: patch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// CreateRefComment creates a commit reference comment to issue.
|
// CreateRefComment creates a commit reference comment to issue.
|
||||||
func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
|
func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
|
||||||
if len(commitSHA) == 0 {
|
if len(commitSHA) == 0 {
|
||||||
|
@ -665,10 +905,11 @@ func GetCommentByID(id int64) (*Comment, error) {
|
||||||
|
|
||||||
// FindCommentsOptions describes the conditions to Find comments
|
// FindCommentsOptions describes the conditions to Find comments
|
||||||
type FindCommentsOptions struct {
|
type FindCommentsOptions struct {
|
||||||
RepoID int64
|
RepoID int64
|
||||||
IssueID int64
|
IssueID int64
|
||||||
Since int64
|
ReviewID int64
|
||||||
Type CommentType
|
Since int64
|
||||||
|
Type CommentType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *FindCommentsOptions) toConds() builder.Cond {
|
func (opts *FindCommentsOptions) toConds() builder.Cond {
|
||||||
|
@ -679,6 +920,9 @@ func (opts *FindCommentsOptions) toConds() builder.Cond {
|
||||||
if opts.IssueID > 0 {
|
if opts.IssueID > 0 {
|
||||||
cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
|
cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
|
||||||
}
|
}
|
||||||
|
if opts.ReviewID > 0 {
|
||||||
|
cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID})
|
||||||
|
}
|
||||||
if opts.Since > 0 {
|
if opts.Since > 0 {
|
||||||
cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
|
cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
|
||||||
}
|
}
|
||||||
|
@ -819,3 +1063,75 @@ func DeleteComment(doer *User, comment *Comment) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
|
||||||
|
type CodeComments map[string]map[int64][]*Comment
|
||||||
|
|
||||||
|
func fetchCodeComments(e Engine, issue *Issue, currentUser *User) (CodeComments, error) {
|
||||||
|
return fetchCodeCommentsByReview(e, issue, currentUser, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review *Review) (CodeComments, error) {
|
||||||
|
pathToLineToComment := make(CodeComments)
|
||||||
|
if review == nil {
|
||||||
|
review = &Review{ID: 0}
|
||||||
|
}
|
||||||
|
//Find comments
|
||||||
|
opts := FindCommentsOptions{
|
||||||
|
Type: CommentTypeCode,
|
||||||
|
IssueID: issue.ID,
|
||||||
|
ReviewID: review.ID,
|
||||||
|
}
|
||||||
|
conds := opts.toConds()
|
||||||
|
if review.ID == 0 {
|
||||||
|
conds = conds.And(builder.Eq{"invalidated": false})
|
||||||
|
}
|
||||||
|
|
||||||
|
var comments []*Comment
|
||||||
|
if err := e.Where(conds).
|
||||||
|
Asc("comment.created_unix").
|
||||||
|
Asc("comment.id").
|
||||||
|
Find(&comments); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := issue.loadRepo(e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Find all reviews by ReviewID
|
||||||
|
reviews := make(map[int64]*Review)
|
||||||
|
var ids = make([]int64, 0, len(comments))
|
||||||
|
for _, comment := range comments {
|
||||||
|
if comment.ReviewID != 0 {
|
||||||
|
ids = append(ids, comment.ReviewID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.In("id", ids).Find(&reviews); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, comment := range comments {
|
||||||
|
if re, ok := reviews[comment.ReviewID]; ok && re != nil {
|
||||||
|
// If the review is pending only the author can see the comments (except the review is set)
|
||||||
|
if review.ID == 0 {
|
||||||
|
if re.Type == ReviewTypePending &&
|
||||||
|
(currentUser == nil || currentUser.ID != re.ReviewerID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
comment.Review = re
|
||||||
|
}
|
||||||
|
|
||||||
|
comment.RenderedContent = string(markdown.Render([]byte(comment.Content), issue.Repo.Link(),
|
||||||
|
issue.Repo.ComposeMetas()))
|
||||||
|
if pathToLineToComment[comment.TreePath] == nil {
|
||||||
|
pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
|
||||||
|
}
|
||||||
|
pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
|
||||||
|
}
|
||||||
|
return pathToLineToComment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
|
||||||
|
func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) {
|
||||||
|
return fetchCodeComments(x, issue, currentUser)
|
||||||
|
}
|
||||||
|
|
|
@ -39,3 +39,21 @@ func TestCreateComment(t *testing.T) {
|
||||||
updatedIssue := AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue)
|
updatedIssue := AssertExistsAndLoadBean(t, &Issue{ID: issue.ID}).(*Issue)
|
||||||
AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetchCodeComments(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
|
||||||
|
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
|
||||||
|
res, err := FetchCodeComments(issue, user)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, res, "README.md")
|
||||||
|
assert.Contains(t, res["README.md"], int64(4))
|
||||||
|
assert.Len(t, res["README.md"][4], 1)
|
||||||
|
assert.Equal(t, int64(4), res["README.md"][4][0].ID)
|
||||||
|
|
||||||
|
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||||
|
res, err = FetchCodeComments(issue, user2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, res, 1)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -181,7 +181,7 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
||||||
// GetMilestonesByRepoID returns all milestones of a repository.
|
// GetMilestonesByRepoID returns all milestones of a repository.
|
||||||
func GetMilestonesByRepoID(repoID int64) (MilestoneList, error) {
|
func GetMilestonesByRepoID(repoID int64) (MilestoneList, error) {
|
||||||
miles := make([]*Milestone, 0, 10)
|
miles := make([]*Milestone, 0, 10)
|
||||||
return miles, x.Where("repo_id = ?", repoID).Find(&miles)
|
return miles, x.Where("repo_id = ?", repoID).Asc("deadline_unix").Find(&miles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMilestones returns a list of milestones of given repository and status.
|
// GetMilestones returns a list of milestones of given repository and status.
|
||||||
|
|
|
@ -192,6 +192,14 @@ var migrations = []Migration{
|
||||||
NewMigration("Reformat and remove incorrect topics", reformatAndRemoveIncorrectTopics),
|
NewMigration("Reformat and remove incorrect topics", reformatAndRemoveIncorrectTopics),
|
||||||
// v69 -> v70
|
// v69 -> v70
|
||||||
NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
|
NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable),
|
||||||
|
// v70 -> v71
|
||||||
|
NewMigration("add issue_dependencies", addIssueDependencies),
|
||||||
|
// v71 -> v72
|
||||||
|
NewMigration("protect each scratch token", addScratchHash),
|
||||||
|
// v72 -> v73
|
||||||
|
NewMigration("add review", addReview),
|
||||||
|
// v73 -> v74
|
||||||
|
NewMigration("add must_change_password column for users table", addMustChangePassword),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/generate"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addScratchHash(x *xorm.Engine) error {
|
||||||
|
// TwoFactor see models/twofactor.go
|
||||||
|
type TwoFactor struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
UID int64 `xorm:"UNIQUE"`
|
||||||
|
Secret string
|
||||||
|
ScratchToken string
|
||||||
|
ScratchSalt string
|
||||||
|
ScratchHash string
|
||||||
|
LastUsedPasscode string `xorm:"VARCHAR(10)"`
|
||||||
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := x.Sync2(new(TwoFactor)); err != nil {
|
||||||
|
return fmt.Errorf("Sync2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
if err := sess.Begin(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform all tokens to hashes
|
||||||
|
const batchSize = 100
|
||||||
|
for start := 0; ; start += batchSize {
|
||||||
|
tfas := make([]*TwoFactor, 0, batchSize)
|
||||||
|
if err := x.Limit(batchSize, start).Find(&tfas); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(tfas) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tfa := range tfas {
|
||||||
|
// generate salt
|
||||||
|
salt, err := generate.GetRandomString(10)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tfa.ScratchSalt = salt
|
||||||
|
tfa.ScratchHash = hashToken(tfa.ScratchToken, salt)
|
||||||
|
|
||||||
|
if _, err := sess.ID(tfa.ID).Cols("scratch_salt, scratch_hash").Update(tfa); err != nil {
|
||||||
|
return fmt.Errorf("couldn't add in scratch_hash and scratch_salt: %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, "two_factor", "scratch_token"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sess.Commit()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashToken(token, salt string) string {
|
||||||
|
tempHash := pbkdf2.Key([]byte(token), []byte(salt), 10000, 50, sha256.New)
|
||||||
|
return fmt.Sprintf("%x", tempHash)
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// 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 addReview(x *xorm.Engine) error {
|
||||||
|
// Review see models/review.go
|
||||||
|
type Review struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
Type string
|
||||||
|
ReviewerID int64 `xorm:"index"`
|
||||||
|
IssueID int64 `xorm:"index"`
|
||||||
|
Content string
|
||||||
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := x.Sync2(new(Review)); err != nil {
|
||||||
|
return fmt.Errorf("Sync2: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// 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 (
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addMustChangePassword(x *xorm.Engine) error {
|
||||||
|
// User see models/user.go
|
||||||
|
type User struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
MustChangePassword bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync2(new(User))
|
||||||
|
}
|
|
@ -118,11 +118,13 @@ 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),
|
new(TeamUnit),
|
||||||
|
new(Review),
|
||||||
)
|
)
|
||||||
|
|
||||||
gonicNames := []string{"SSL", "UID"}
|
gonicNames := []string{"SSL", "UID"}
|
||||||
|
@ -153,7 +155,7 @@ func LoadConfigs() {
|
||||||
if len(DbCfg.Passwd) == 0 {
|
if len(DbCfg.Passwd) == 0 {
|
||||||
DbCfg.Passwd = sec.Key("PASSWD").String()
|
DbCfg.Passwd = sec.Key("PASSWD").String()
|
||||||
}
|
}
|
||||||
DbCfg.SSLMode = sec.Key("SSL_MODE").String()
|
DbCfg.SSLMode = sec.Key("SSL_MODE").MustString("disable")
|
||||||
DbCfg.Path = sec.Key("PATH").MustString("data/gitea.db")
|
DbCfg.Path = sec.Key("PATH").MustString("data/gitea.db")
|
||||||
DbCfg.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500)
|
DbCfg.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500)
|
||||||
|
|
||||||
|
@ -220,13 +222,16 @@ func getEngine() (*xorm.Engine, error) {
|
||||||
}
|
}
|
||||||
switch DbCfg.Type {
|
switch DbCfg.Type {
|
||||||
case "mysql":
|
case "mysql":
|
||||||
|
connType := "tcp"
|
||||||
if DbCfg.Host[0] == '/' { // looks like a unix socket
|
if DbCfg.Host[0] == '/' { // looks like a unix socket
|
||||||
connStr = fmt.Sprintf("%s:%s@unix(%s)/%s%scharset=utf8&parseTime=true",
|
connType = "unix"
|
||||||
DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name, Param)
|
|
||||||
} else {
|
|
||||||
connStr = fmt.Sprintf("%s:%s@tcp(%s)/%s%scharset=utf8&parseTime=true",
|
|
||||||
DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name, Param)
|
|
||||||
}
|
}
|
||||||
|
tls := DbCfg.SSLMode
|
||||||
|
if tls == "disable" { // allow (Postgres-inspired) default value to work in MySQL
|
||||||
|
tls = "false"
|
||||||
|
}
|
||||||
|
connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=utf8&parseTime=true&tls=%s",
|
||||||
|
DbCfg.User, DbCfg.Passwd, connType, DbCfg.Host, DbCfg.Name, Param, tls)
|
||||||
case "postgres":
|
case "postgres":
|
||||||
connStr = getPostgreSQLConnectionString(DbCfg.Host, DbCfg.User, DbCfg.Passwd, DbCfg.Name, Param, DbCfg.SSLMode)
|
connStr = getPostgreSQLConnectionString(DbCfg.Host, DbCfg.User, DbCfg.Passwd, DbCfg.Name, Param, DbCfg.SSLMode)
|
||||||
case "mssql":
|
case "mssql":
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
|
|
@ -182,10 +182,6 @@ func CreateOrganization(org, owner *User) (err error) {
|
||||||
return fmt.Errorf("insert team-user relation: %v", err)
|
return fmt.Errorf("insert team-user relation: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = os.MkdirAll(UserPath(org.Name), os.ModePerm); err != nil {
|
|
||||||
return fmt.Errorf("create directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -214,7 +214,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
if pr.Status != PullRequestStatusChecking {
|
if pr.Status != PullRequestStatusChecking {
|
||||||
mergeable := pr.Status != PullRequestStatusConflict
|
mergeable := pr.Status != PullRequestStatusConflict && !pr.IsWorkInProgress()
|
||||||
apiPullRequest.Mergeable = mergeable
|
apiPullRequest.Mergeable = mergeable
|
||||||
}
|
}
|
||||||
if pr.HasMerged {
|
if pr.HasMerged {
|
||||||
|
@ -1075,10 +1075,7 @@ func (prs PullRequestList) loadAttributes(e Engine) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load issues.
|
// Load issues.
|
||||||
issueIDs := make([]int64, 0, len(prs))
|
issueIDs := prs.getIssueIDs()
|
||||||
for i := range prs {
|
|
||||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
|
||||||
}
|
|
||||||
issues := make([]*Issue, 0, len(issueIDs))
|
issues := make([]*Issue, 0, len(issueIDs))
|
||||||
if err := e.
|
if err := e.
|
||||||
Where("id > 0").
|
Where("id > 0").
|
||||||
|
@ -1097,11 +1094,44 @@ func (prs PullRequestList) loadAttributes(e Engine) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (prs PullRequestList) getIssueIDs() []int64 {
|
||||||
|
issueIDs := make([]int64, 0, len(prs))
|
||||||
|
for i := range prs {
|
||||||
|
issueIDs = append(issueIDs, prs[i].IssueID)
|
||||||
|
}
|
||||||
|
return issueIDs
|
||||||
|
}
|
||||||
|
|
||||||
// LoadAttributes load all the prs attributes
|
// LoadAttributes load all the prs attributes
|
||||||
func (prs PullRequestList) LoadAttributes() error {
|
func (prs PullRequestList) LoadAttributes() error {
|
||||||
return prs.loadAttributes(x)
|
return prs.loadAttributes(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (prs PullRequestList) invalidateCodeComments(e Engine, doer *User, repo *git.Repository, branch string) error {
|
||||||
|
if len(prs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
issueIDs := prs.getIssueIDs()
|
||||||
|
var codeComments []*Comment
|
||||||
|
if err := e.
|
||||||
|
Where("type = ? and invalidated = ?", CommentTypeCode, false).
|
||||||
|
In("issue_id", issueIDs).
|
||||||
|
Find(&codeComments); err != nil {
|
||||||
|
return fmt.Errorf("find code comments: %v", err)
|
||||||
|
}
|
||||||
|
for _, comment := range codeComments {
|
||||||
|
if err := comment.CheckInvalidation(repo, doer, branch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
|
||||||
|
func (prs PullRequestList) InvalidateCodeComments(doer *User, repo *git.Repository, branch string) error {
|
||||||
|
return prs.invalidateCodeComments(x, doer, repo, branch)
|
||||||
|
}
|
||||||
|
|
||||||
func addHeadRepoTasks(prs []*PullRequest) {
|
func addHeadRepoTasks(prs []*PullRequest) {
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID)
|
log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID)
|
||||||
|
@ -1128,10 +1158,13 @@ func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSync {
|
if isSync {
|
||||||
if err = PullRequestList(prs).LoadAttributes(); err != nil {
|
requests := PullRequestList(prs)
|
||||||
|
if err = requests.LoadAttributes(); err != nil {
|
||||||
log.Error(4, "PullRequestList.LoadAttributes: %v", err)
|
log.Error(4, "PullRequestList.LoadAttributes: %v", err)
|
||||||
}
|
}
|
||||||
|
if invalidationErr := checkForInvalidation(requests, repoID, doer, branch); invalidationErr != nil {
|
||||||
|
log.Error(4, "checkForInvalidation: %v", invalidationErr)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, pr := range prs {
|
for _, pr := range prs {
|
||||||
pr.Issue.PullRequest = pr
|
pr.Issue.PullRequest = pr
|
||||||
|
@ -1152,6 +1185,7 @@ func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool
|
||||||
go HookQueue.Add(pr.Issue.Repo.ID)
|
go HookQueue.Add(pr.Issue.Repo.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addHeadRepoTasks(prs)
|
addHeadRepoTasks(prs)
|
||||||
|
@ -1167,6 +1201,24 @@ func AddTestPullRequestTask(doer *User, repoID int64, branch string, isSync bool
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkForInvalidation(requests PullRequestList, repoID int64, doer *User, branch string) error {
|
||||||
|
repo, err := GetRepositoryByID(repoID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetRepositoryByID: %v", err)
|
||||||
|
}
|
||||||
|
gitRepo, err := git.OpenRepository(repo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("git.OpenRepository: %v", err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
err := requests.InvalidateCodeComments(doer, gitRepo, branch)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(4, "PullRequestList.InvalidateCodeComments: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ChangeUsernameInPullRequests changes the name of head_user_name
|
// ChangeUsernameInPullRequests changes the name of head_user_name
|
||||||
func ChangeUsernameInPullRequests(oldUserName, newUserName string) error {
|
func ChangeUsernameInPullRequests(oldUserName, newUserName string) error {
|
||||||
pr := PullRequest{
|
pr := PullRequest{
|
||||||
|
@ -1195,6 +1247,37 @@ func (pr *PullRequest) checkAndUpdateStatus() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
|
||||||
|
func (pr *PullRequest) IsWorkInProgress() bool {
|
||||||
|
if err := pr.LoadIssue(); err != nil {
|
||||||
|
log.Error(4, "LoadIssue: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
|
||||||
|
if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
|
||||||
|
// It returns an empty string when none were found
|
||||||
|
func (pr *PullRequest) GetWorkInProgressPrefix() string {
|
||||||
|
if err := pr.LoadIssue(); err != nil {
|
||||||
|
log.Error(4, "LoadIssue: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
|
||||||
|
if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), prefix) {
|
||||||
|
return pr.Issue.Title[0:len(prefix)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// TestPullRequests checks and tests untested patches of pull requests.
|
// TestPullRequests checks and tests untested patches of pull requests.
|
||||||
// TODO: test more pull requests at same time.
|
// TODO: test more pull requests at same time.
|
||||||
func TestPullRequests() {
|
func TestPullRequests() {
|
||||||
|
|
|
@ -237,3 +237,34 @@ func TestChangeUsernameInPullRequests(t *testing.T) {
|
||||||
}
|
}
|
||||||
CheckConsistencyFor(t, &PullRequest{})
|
CheckConsistencyFor(t, &PullRequest{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
pr := AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest)
|
||||||
|
pr.LoadIssue()
|
||||||
|
|
||||||
|
assert.False(t, pr.IsWorkInProgress())
|
||||||
|
|
||||||
|
pr.Issue.Title = "WIP: " + pr.Issue.Title
|
||||||
|
assert.True(t, pr.IsWorkInProgress())
|
||||||
|
|
||||||
|
pr.Issue.Title = "[wip]: " + pr.Issue.Title
|
||||||
|
assert.True(t, pr.IsWorkInProgress())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
pr := AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest)
|
||||||
|
pr.LoadIssue()
|
||||||
|
|
||||||
|
assert.Empty(t, pr.GetWorkInProgressPrefix())
|
||||||
|
|
||||||
|
original := pr.Issue.Title
|
||||||
|
pr.Issue.Title = "WIP: " + original
|
||||||
|
assert.Equal(t, "WIP:", pr.GetWorkInProgressPrefix())
|
||||||
|
|
||||||
|
pr.Issue.Title = "[wip] " + original
|
||||||
|
assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix())
|
||||||
|
}
|
||||||
|
|
|
@ -781,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))
|
||||||
}
|
}
|
||||||
|
@ -1345,7 +1345,11 @@ 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 {
|
} else if tp == UnitTypePullRequests {
|
||||||
units = append(units, RepoUnit{
|
units = append(units, RepoUnit{
|
||||||
|
|
|
@ -131,6 +131,8 @@ type SearchRepoOptions struct {
|
||||||
// True -> include just mirrors
|
// True -> include just mirrors
|
||||||
// False -> include just non-mirrors
|
// False -> include just non-mirrors
|
||||||
Mirror util.OptionalBool
|
Mirror util.OptionalBool
|
||||||
|
// only search topic name
|
||||||
|
TopicOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//SearchOrderBy is used to sort the result
|
//SearchOrderBy is used to sort the result
|
||||||
|
@ -184,7 +186,7 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
|
||||||
|
|
||||||
if opts.Collaborate != util.OptionalBoolFalse {
|
if opts.Collaborate != util.OptionalBoolFalse {
|
||||||
collaborateCond := builder.And(
|
collaborateCond := builder.And(
|
||||||
builder.Expr("id IN (SELECT repo_id FROM `access` WHERE access.user_id = ?)", opts.OwnerID),
|
builder.Expr("repository.id IN (SELECT repo_id FROM `access` WHERE access.user_id = ?)", opts.OwnerID),
|
||||||
builder.Neq{"owner_id": opts.OwnerID})
|
builder.Neq{"owner_id": opts.OwnerID})
|
||||||
if !opts.Private {
|
if !opts.Private {
|
||||||
collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
|
collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
|
||||||
|
@ -202,7 +204,14 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Keyword != "" {
|
if opts.Keyword != "" {
|
||||||
cond = cond.And(builder.Like{"lower_name", strings.ToLower(opts.Keyword)})
|
var keywordCond = builder.NewCond()
|
||||||
|
if opts.TopicOnly {
|
||||||
|
keywordCond = keywordCond.Or(builder.Like{"topic.name", strings.ToLower(opts.Keyword)})
|
||||||
|
} else {
|
||||||
|
keywordCond = keywordCond.Or(builder.Like{"lower_name", strings.ToLower(opts.Keyword)})
|
||||||
|
keywordCond = keywordCond.Or(builder.Like{"topic.name", strings.ToLower(opts.Keyword)})
|
||||||
|
}
|
||||||
|
cond = cond.And(keywordCond)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Fork != util.OptionalBoolNone {
|
if opts.Fork != util.OptionalBoolNone {
|
||||||
|
@ -224,9 +233,15 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
|
||||||
sess.Join("INNER", "star", "star.repo_id = repository.id")
|
sess.Join("INNER", "star", "star.repo_id = repository.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Keyword != "" {
|
||||||
|
sess.Join("LEFT", "repo_topic", "repo_topic.repo_id = repository.id")
|
||||||
|
sess.Join("LEFT", "topic", "repo_topic.topic_id = topic.id")
|
||||||
|
}
|
||||||
|
|
||||||
count, err := sess.
|
count, err := sess.
|
||||||
Where(cond).
|
Where(cond).
|
||||||
Count(new(Repository))
|
Count(new(Repository))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("Count: %v", err)
|
return nil, 0, fmt.Errorf("Count: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -236,11 +251,23 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
|
||||||
sess.Join("INNER", "star", "star.repo_id = repository.id")
|
sess.Join("INNER", "star", "star.repo_id = repository.id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Keyword != "" {
|
||||||
|
sess.Join("LEFT", "repo_topic", "repo_topic.repo_id = repository.id")
|
||||||
|
sess.Join("LEFT", "topic", "repo_topic.topic_id = topic.id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Keyword != "" {
|
||||||
|
sess.Select("repository.*")
|
||||||
|
sess.GroupBy("repository.id")
|
||||||
|
sess.OrderBy("repository." + opts.OrderBy.String())
|
||||||
|
} else {
|
||||||
|
sess.OrderBy(opts.OrderBy.String())
|
||||||
|
}
|
||||||
|
|
||||||
repos := make(RepositoryList, 0, opts.PageSize)
|
repos := make(RepositoryList, 0, opts.PageSize)
|
||||||
if err = sess.
|
if err = sess.
|
||||||
Where(cond).
|
Where(cond).
|
||||||
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
|
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
|
||||||
OrderBy(opts.OrderBy.String()).
|
|
||||||
Find(&repos); err != nil {
|
Find(&repos); err != nil {
|
||||||
return nil, 0, fmt.Errorf("Repo: %v", err)
|
return nil, 0, fmt.Errorf("Repo: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: 17},
|
count: 19},
|
||||||
{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: 21},
|
count: 23},
|
||||||
{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: 17},
|
count: 19},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
@ -222,3 +222,28 @@ func TestSearchRepositoryByName(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSearchRepositoryByTopicName(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
opts *SearchRepoOptions
|
||||||
|
count int
|
||||||
|
}{
|
||||||
|
{name: "AllPublic/SearchPublicRepositoriesFromTopicAndName",
|
||||||
|
opts: &SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"},
|
||||||
|
count: 2},
|
||||||
|
{name: "AllPublic/OnlySearchPublicRepositoriesFromTopic",
|
||||||
|
opts: &SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true},
|
||||||
|
count: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
_, count, err := SearchRepositoryByName(testCase.opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(testCase.count), count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
@ -6,6 +7,7 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/git"
|
"code.gitea.io/git"
|
||||||
|
@ -119,8 +121,68 @@ func (m *Mirror) SaveAddress(addr string) error {
|
||||||
return cfg.SaveToIndent(configPath, "\t")
|
return cfg.SaveToIndent(configPath, "\t")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gitShortEmptySha Git short empty SHA
|
||||||
|
const gitShortEmptySha = "0000000"
|
||||||
|
|
||||||
|
// mirrorSyncResult contains information of a updated reference.
|
||||||
|
// If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
|
||||||
|
// If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty.
|
||||||
|
type mirrorSyncResult struct {
|
||||||
|
refName string
|
||||||
|
oldCommitID string
|
||||||
|
newCommitID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseRemoteUpdateOutput detects create, update and delete operations of references from upstream.
|
||||||
|
func parseRemoteUpdateOutput(output string) []*mirrorSyncResult {
|
||||||
|
results := make([]*mirrorSyncResult, 0, 3)
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for i := range lines {
|
||||||
|
// Make sure reference name is presented before continue
|
||||||
|
idx := strings.Index(lines[i], "-> ")
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
refName := lines[i][idx+3:]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(lines[i], " * "): // New reference
|
||||||
|
results = append(results, &mirrorSyncResult{
|
||||||
|
refName: refName,
|
||||||
|
oldCommitID: gitShortEmptySha,
|
||||||
|
})
|
||||||
|
case strings.HasPrefix(lines[i], " - "): // Delete reference
|
||||||
|
results = append(results, &mirrorSyncResult{
|
||||||
|
refName: refName,
|
||||||
|
newCommitID: gitShortEmptySha,
|
||||||
|
})
|
||||||
|
case strings.HasPrefix(lines[i], " "): // New commits of a reference
|
||||||
|
delimIdx := strings.Index(lines[i][3:], " ")
|
||||||
|
if delimIdx == -1 {
|
||||||
|
log.Error(2, "SHA delimiter not found: %q", lines[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
shas := strings.Split(lines[i][3:delimIdx+3], "..")
|
||||||
|
if len(shas) != 2 {
|
||||||
|
log.Error(2, "Expect two SHAs but not what found: %q", lines[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results = append(results, &mirrorSyncResult{
|
||||||
|
refName: refName,
|
||||||
|
oldCommitID: shas[0],
|
||||||
|
newCommitID: shas[1],
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
// runSync returns true if sync finished without error.
|
// runSync returns true if sync finished without error.
|
||||||
func (m *Mirror) runSync() bool {
|
func (m *Mirror) runSync() ([]*mirrorSyncResult, bool) {
|
||||||
repoPath := m.Repo.RepoPath()
|
repoPath := m.Repo.RepoPath()
|
||||||
wikiPath := m.Repo.WikiPath()
|
wikiPath := m.Repo.WikiPath()
|
||||||
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
|
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
|
||||||
|
@ -130,28 +192,30 @@ func (m *Mirror) runSync() bool {
|
||||||
gitArgs = append(gitArgs, "--prune")
|
gitArgs = append(gitArgs, "--prune")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, stderr, err := process.GetManager().ExecDir(
|
_, stderr, err := process.GetManager().ExecDir(
|
||||||
timeout, repoPath, fmt.Sprintf("Mirror.runSync: %s", repoPath),
|
timeout, repoPath, fmt.Sprintf("Mirror.runSync: %s", repoPath),
|
||||||
"git", gitArgs...); err != nil {
|
"git", gitArgs...)
|
||||||
|
if err != nil {
|
||||||
// sanitize the output, since it may contain the remote address, which may
|
// sanitize the output, since it may contain the remote address, which may
|
||||||
// contain a password
|
// contain a password
|
||||||
message, err := sanitizeOutput(stderr, repoPath)
|
message, err := sanitizeOutput(stderr, repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(4, "sanitizeOutput: %v", err)
|
log.Error(4, "sanitizeOutput: %v", err)
|
||||||
return false
|
return nil, false
|
||||||
}
|
}
|
||||||
desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, message)
|
desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, message)
|
||||||
log.Error(4, desc)
|
log.Error(4, desc)
|
||||||
if err = CreateRepositoryNotice(desc); err != nil {
|
if err = CreateRepositoryNotice(desc); err != nil {
|
||||||
log.Error(4, "CreateRepositoryNotice: %v", err)
|
log.Error(4, "CreateRepositoryNotice: %v", err)
|
||||||
}
|
}
|
||||||
return false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
output := stderr
|
||||||
|
|
||||||
gitRepo, err := git.OpenRepository(repoPath)
|
gitRepo, err := git.OpenRepository(repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(4, "OpenRepository: %v", err)
|
log.Error(4, "OpenRepository: %v", err)
|
||||||
return false
|
return nil, false
|
||||||
}
|
}
|
||||||
if err = SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
|
if err = SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
|
||||||
log.Error(4, "Failed to synchronize tags to releases for repository: %v", err)
|
log.Error(4, "Failed to synchronize tags to releases for repository: %v", err)
|
||||||
|
@ -170,21 +234,21 @@ func (m *Mirror) runSync() bool {
|
||||||
message, err := sanitizeOutput(stderr, wikiPath)
|
message, err := sanitizeOutput(stderr, wikiPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(4, "sanitizeOutput: %v", err)
|
log.Error(4, "sanitizeOutput: %v", err)
|
||||||
return false
|
return nil, false
|
||||||
}
|
}
|
||||||
desc := fmt.Sprintf("Failed to update mirror wiki repository '%s': %s", wikiPath, message)
|
desc := fmt.Sprintf("Failed to update mirror wiki repository '%s': %s", wikiPath, message)
|
||||||
log.Error(4, desc)
|
log.Error(4, desc)
|
||||||
if err = CreateRepositoryNotice(desc); err != nil {
|
if err = CreateRepositoryNotice(desc); err != nil {
|
||||||
log.Error(4, "CreateRepositoryNotice: %v", err)
|
log.Error(4, "CreateRepositoryNotice: %v", err)
|
||||||
}
|
}
|
||||||
return false
|
return nil, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
branches, err := m.Repo.GetBranches()
|
branches, err := m.Repo.GetBranches()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(4, "GetBranches: %v", err)
|
log.Error(4, "GetBranches: %v", err)
|
||||||
return false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range branches {
|
for i := range branches {
|
||||||
|
@ -192,7 +256,7 @@ func (m *Mirror) runSync() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
m.UpdatedUnix = util.TimeStampNow()
|
m.UpdatedUnix = util.TimeStampNow()
|
||||||
return true
|
return parseRemoteUpdateOutput(output), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMirrorByRepoID(e Engine, repoID int64) (*Mirror, error) {
|
func getMirrorByRepoID(e Engine, repoID int64) (*Mirror, error) {
|
||||||
|
@ -268,7 +332,8 @@ func SyncMirrors() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.runSync() {
|
results, ok := m.runSync()
|
||||||
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,6 +343,66 @@ func SyncMirrors() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gitRepo *git.Repository
|
||||||
|
if len(results) == 0 {
|
||||||
|
log.Trace("SyncMirrors [repo_id: %d]: no commits fetched", m.RepoID)
|
||||||
|
} else {
|
||||||
|
gitRepo, err = git.OpenRepository(m.Repo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
log.Error(2, "OpenRepository [%d]: %v", m.RepoID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
// Discard GitHub pull requests, i.e. refs/pull/*
|
||||||
|
if strings.HasPrefix(result.refName, "refs/pull/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create reference
|
||||||
|
if result.oldCommitID == gitShortEmptySha {
|
||||||
|
if err = MirrorSyncCreateAction(m.Repo, result.refName); err != nil {
|
||||||
|
log.Error(2, "MirrorSyncCreateAction [repo_id: %d]: %v", m.RepoID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete reference
|
||||||
|
if result.newCommitID == gitShortEmptySha {
|
||||||
|
if err = MirrorSyncDeleteAction(m.Repo, result.refName); err != nil {
|
||||||
|
log.Error(2, "MirrorSyncDeleteAction [repo_id: %d]: %v", m.RepoID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push commits
|
||||||
|
oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(2, "GetFullCommitID [%d]: %v", m.RepoID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(2, "GetFullCommitID [%d]: %v", m.RepoID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(2, "CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = MirrorSyncPushAction(m.Repo, MirrorSyncPushActionOptions{
|
||||||
|
RefName: result.refName,
|
||||||
|
OldCommitID: oldCommitID,
|
||||||
|
NewCommitID: newCommitID,
|
||||||
|
Commits: ListToPushCommits(commits),
|
||||||
|
}); err != nil {
|
||||||
|
log.Error(2, "MirrorSyncPushAction [repo_id: %d]: %v", m.RepoID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get latest commit date and update to current repository updated time
|
// Get latest commit date and update to current repository updated time
|
||||||
commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath())
|
commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -165,7 +166,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,256 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
|
||||||
|
"github.com/go-xorm/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReviewType defines the sort of feedback a review gives
|
||||||
|
type ReviewType int
|
||||||
|
|
||||||
|
// ReviewTypeUnknown unknown review type
|
||||||
|
const ReviewTypeUnknown ReviewType = -1
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ReviewTypePending is a review which is not published yet
|
||||||
|
ReviewTypePending ReviewType = iota
|
||||||
|
// ReviewTypeApprove approves changes
|
||||||
|
ReviewTypeApprove
|
||||||
|
// ReviewTypeComment gives general feedback
|
||||||
|
ReviewTypeComment
|
||||||
|
// ReviewTypeReject gives feedback blocking merge
|
||||||
|
ReviewTypeReject
|
||||||
|
)
|
||||||
|
|
||||||
|
// Icon returns the corresponding icon for the review type
|
||||||
|
func (rt ReviewType) Icon() string {
|
||||||
|
switch rt {
|
||||||
|
case ReviewTypeApprove:
|
||||||
|
return "eye"
|
||||||
|
case ReviewTypeReject:
|
||||||
|
return "x"
|
||||||
|
default:
|
||||||
|
case ReviewTypeComment:
|
||||||
|
case ReviewTypeUnknown:
|
||||||
|
return "comment"
|
||||||
|
}
|
||||||
|
return "comment"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Review represents collection of code comments giving feedback for a PR
|
||||||
|
type Review struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
Type ReviewType
|
||||||
|
Reviewer *User `xorm:"-"`
|
||||||
|
ReviewerID int64 `xorm:"index"`
|
||||||
|
Issue *Issue `xorm:"-"`
|
||||||
|
IssueID int64 `xorm:"index"`
|
||||||
|
Content string
|
||||||
|
|
||||||
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
|
||||||
|
// CodeComments are the initial code comments of the review
|
||||||
|
CodeComments CodeComments `xorm:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Review) loadCodeComments(e Engine) (err error) {
|
||||||
|
r.CodeComments, err = fetchCodeCommentsByReview(e, r.Issue, nil, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCodeComments loads CodeComments
|
||||||
|
func (r *Review) LoadCodeComments() error {
|
||||||
|
return r.loadCodeComments(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Review) loadIssue(e Engine) (err error) {
|
||||||
|
r.Issue, err = getIssueByID(e, r.IssueID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Review) loadReviewer(e Engine) (err error) {
|
||||||
|
if r.ReviewerID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.Reviewer, err = getUserByID(e, r.ReviewerID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Review) loadAttributes(e Engine) (err error) {
|
||||||
|
if err = r.loadReviewer(e); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = r.loadIssue(e); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAttributes loads all attributes except CodeComments
|
||||||
|
func (r *Review) LoadAttributes() error {
|
||||||
|
return r.loadAttributes(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish will send notifications / actions to participants for all code comments; parts are concurrent
|
||||||
|
func (r *Review) Publish() error {
|
||||||
|
return r.publish(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Review) publish(e *xorm.Engine) error {
|
||||||
|
if r.Type == ReviewTypePending || r.Type == ReviewTypeUnknown {
|
||||||
|
return fmt.Errorf("review cannot be published if type is pending or unknown")
|
||||||
|
}
|
||||||
|
if r.Issue == nil {
|
||||||
|
if err := r.loadIssue(e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.Issue.loadRepo(e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(r.CodeComments) == 0 {
|
||||||
|
if err := r.loadCodeComments(e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, lines := range r.CodeComments {
|
||||||
|
for _, comments := range lines {
|
||||||
|
for _, comment := range comments {
|
||||||
|
go func(en *xorm.Engine, review *Review, comm *Comment) {
|
||||||
|
sess := en.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
if err := sendCreateCommentAction(sess, &CreateCommentOptions{
|
||||||
|
Doer: comm.Poster,
|
||||||
|
Issue: review.Issue,
|
||||||
|
Repo: review.Issue.Repo,
|
||||||
|
Type: comm.Type,
|
||||||
|
Content: comm.Content,
|
||||||
|
}, comm); err != nil {
|
||||||
|
log.Warn("sendCreateCommentAction: %v", err)
|
||||||
|
}
|
||||||
|
}(e, r, comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getReviewByID(e Engine, id int64) (*Review, error) {
|
||||||
|
review := new(Review)
|
||||||
|
if has, err := e.ID(id).Get(review); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrReviewNotExist{ID: id}
|
||||||
|
} else {
|
||||||
|
return review, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReviewByID returns the review by the given ID
|
||||||
|
func GetReviewByID(id int64) (*Review, error) {
|
||||||
|
return getReviewByID(x, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindReviewOptions represent possible filters to find reviews
|
||||||
|
type FindReviewOptions struct {
|
||||||
|
Type ReviewType
|
||||||
|
IssueID int64
|
||||||
|
ReviewerID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *FindReviewOptions) toCond() builder.Cond {
|
||||||
|
var cond = builder.NewCond()
|
||||||
|
if opts.IssueID > 0 {
|
||||||
|
cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
|
||||||
|
}
|
||||||
|
if opts.ReviewerID > 0 {
|
||||||
|
cond = cond.And(builder.Eq{"reviewer_id": opts.ReviewerID})
|
||||||
|
}
|
||||||
|
if opts.Type != ReviewTypeUnknown {
|
||||||
|
cond = cond.And(builder.Eq{"type": opts.Type})
|
||||||
|
}
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
func findReviews(e Engine, opts FindReviewOptions) ([]*Review, error) {
|
||||||
|
reviews := make([]*Review, 0, 10)
|
||||||
|
sess := e.Where(opts.toCond())
|
||||||
|
return reviews, sess.
|
||||||
|
Asc("created_unix").
|
||||||
|
Asc("id").
|
||||||
|
Find(&reviews)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindReviews returns reviews passing FindReviewOptions
|
||||||
|
func FindReviews(opts FindReviewOptions) ([]*Review, error) {
|
||||||
|
return findReviews(x, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateReviewOptions represent the options to create a review. Type, Issue and Reviewer are required.
|
||||||
|
type CreateReviewOptions struct {
|
||||||
|
Content string
|
||||||
|
Type ReviewType
|
||||||
|
Issue *Issue
|
||||||
|
Reviewer *User
|
||||||
|
}
|
||||||
|
|
||||||
|
func createReview(e Engine, opts CreateReviewOptions) (*Review, error) {
|
||||||
|
review := &Review{
|
||||||
|
Type: opts.Type,
|
||||||
|
Issue: opts.Issue,
|
||||||
|
IssueID: opts.Issue.ID,
|
||||||
|
Reviewer: opts.Reviewer,
|
||||||
|
ReviewerID: opts.Reviewer.ID,
|
||||||
|
Content: opts.Content,
|
||||||
|
}
|
||||||
|
if _, err := e.Insert(review); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return review, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateReview creates a new review based on opts
|
||||||
|
func CreateReview(opts CreateReviewOptions) (*Review, error) {
|
||||||
|
return createReview(x, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentReview(e Engine, reviewer *User, issue *Issue) (*Review, error) {
|
||||||
|
if reviewer == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
reviews, err := findReviews(e, FindReviewOptions{
|
||||||
|
Type: ReviewTypePending,
|
||||||
|
IssueID: issue.ID,
|
||||||
|
ReviewerID: reviewer.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(reviews) == 0 {
|
||||||
|
return nil, ErrReviewNotExist{}
|
||||||
|
}
|
||||||
|
return reviews[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentReview returns the current pending review of reviewer for given issue
|
||||||
|
func GetCurrentReview(reviewer *User, issue *Issue) (*Review, error) {
|
||||||
|
return getCurrentReview(x, reviewer, issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateReview will update all cols of the given review in db
|
||||||
|
func UpdateReview(r *Review) error {
|
||||||
|
if _, err := x.ID(r.ID).AllCols().Update(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetReviewByID(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
review, err := GetReviewByID(1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "Demo Review", review.Content)
|
||||||
|
assert.Equal(t, ReviewTypeApprove, review.Type)
|
||||||
|
|
||||||
|
_, err = GetReviewByID(23892)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, IsErrReviewNotExist(err), "IsErrReviewNotExist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReview_LoadAttributes(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
review := AssertExistsAndLoadBean(t, &Review{ID: 1}).(*Review)
|
||||||
|
assert.NoError(t, review.LoadAttributes())
|
||||||
|
assert.NotNil(t, review.Issue)
|
||||||
|
assert.NotNil(t, review.Reviewer)
|
||||||
|
|
||||||
|
invalidReview1 := AssertExistsAndLoadBean(t, &Review{ID: 2}).(*Review)
|
||||||
|
assert.Error(t, invalidReview1.LoadAttributes())
|
||||||
|
|
||||||
|
invalidReview2 := AssertExistsAndLoadBean(t, &Review{ID: 3}).(*Review)
|
||||||
|
assert.Error(t, invalidReview2.LoadAttributes())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReview_LoadCodeComments(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
review := AssertExistsAndLoadBean(t, &Review{ID: 4}).(*Review)
|
||||||
|
assert.NoError(t, review.LoadAttributes())
|
||||||
|
assert.NoError(t, review.LoadCodeComments())
|
||||||
|
assert.Len(t, review.CodeComments, 1)
|
||||||
|
assert.Equal(t, int64(4), review.CodeComments["README.md"][int64(4)][0].Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReviewType_Icon(t *testing.T) {
|
||||||
|
assert.Equal(t, "eye", ReviewTypeApprove.Icon())
|
||||||
|
assert.Equal(t, "x", ReviewTypeReject.Icon())
|
||||||
|
assert.Equal(t, "comment", ReviewTypeComment.Icon())
|
||||||
|
assert.Equal(t, "comment", ReviewTypeUnknown.Icon())
|
||||||
|
assert.Equal(t, "comment", ReviewType(4).Icon())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindReviews(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
reviews, err := FindReviews(FindReviewOptions{
|
||||||
|
Type: ReviewTypeApprove,
|
||||||
|
IssueID: 2,
|
||||||
|
ReviewerID: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, reviews, 1)
|
||||||
|
assert.Equal(t, "Demo Review", reviews[0].Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentReview(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
|
||||||
|
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
|
||||||
|
|
||||||
|
review, err := GetCurrentReview(user, issue)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, review)
|
||||||
|
assert.Equal(t, ReviewTypePending, review.Type)
|
||||||
|
assert.Equal(t, "Pending Review", review.Content)
|
||||||
|
|
||||||
|
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
||||||
|
review2, err := GetCurrentReview(user2, issue)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, IsErrReviewNotExist(err))
|
||||||
|
assert.Nil(t, review2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateReview(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
issue := AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue)
|
||||||
|
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User)
|
||||||
|
|
||||||
|
review, err := CreateReview(CreateReviewOptions{
|
||||||
|
Content: "New Review",
|
||||||
|
Type: ReviewTypePending,
|
||||||
|
Issue: issue,
|
||||||
|
Reviewer: user,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "New Review", review.Content)
|
||||||
|
AssertExistsAndLoadBean(t, &Review{Content: "New Review"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateReview(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
review := AssertExistsAndLoadBean(t, &Review{ID: 1}).(*Review)
|
||||||
|
review.Content = "Updated Review"
|
||||||
|
assert.NoError(t, UpdateReview(review))
|
||||||
|
AssertExistsAndLoadBean(t, &Review{ID: 1, Content: "Updated Review"})
|
||||||
|
}
|
|
@ -18,14 +18,14 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
|
||||||
"github.com/go-xorm/xorm"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/Unknwon/com"
|
||||||
|
"github.com/go-xorm/xorm"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -732,7 +732,7 @@ func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey
|
||||||
|
|
||||||
key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
|
key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint, accessMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("addDeployKey: %v", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return key, sess.Commit()
|
return key, sess.Commit()
|
||||||
|
|
|
@ -15,7 +15,7 @@ func TestAddTopic(t *testing.T) {
|
||||||
|
|
||||||
topics, err := FindTopics(&FindTopicOptions{})
|
topics, err := FindTopics(&FindTopicOptions{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, 3, len(topics))
|
assert.EqualValues(t, 4, len(topics))
|
||||||
|
|
||||||
topics, err = FindTopics(&FindTopicOptions{
|
topics, err = FindTopics(&FindTopicOptions{
|
||||||
Limit: 2,
|
Limit: 2,
|
||||||
|
@ -32,7 +32,7 @@ func TestAddTopic(t *testing.T) {
|
||||||
assert.NoError(t, SaveTopics(2, "golang"))
|
assert.NoError(t, SaveTopics(2, "golang"))
|
||||||
topics, err = FindTopics(&FindTopicOptions{})
|
topics, err = FindTopics(&FindTopicOptions{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, 3, len(topics))
|
assert.EqualValues(t, 4, len(topics))
|
||||||
|
|
||||||
topics, err = FindTopics(&FindTopicOptions{
|
topics, err = FindTopics(&FindTopicOptions{
|
||||||
RepoID: 2,
|
RepoID: 2,
|
||||||
|
@ -47,7 +47,7 @@ func TestAddTopic(t *testing.T) {
|
||||||
|
|
||||||
topics, err = FindTopics(&FindTopicOptions{})
|
topics, err = FindTopics(&FindTopicOptions{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, 4, len(topics))
|
assert.EqualValues(t, 5, len(topics))
|
||||||
|
|
||||||
topics, err = FindTopics(&FindTopicOptions{
|
topics, err = FindTopics(&FindTopicOptions{
|
||||||
RepoID: 2,
|
RepoID: 2,
|
||||||
|
|
|
@ -9,12 +9,15 @@ import (
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/generate"
|
"code.gitea.io/gitea/modules/generate"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -26,20 +29,27 @@ type TwoFactor struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UID int64 `xorm:"UNIQUE"`
|
UID int64 `xorm:"UNIQUE"`
|
||||||
Secret string
|
Secret string
|
||||||
ScratchToken string
|
ScratchSalt string
|
||||||
|
ScratchHash string
|
||||||
LastUsedPasscode string `xorm:"VARCHAR(10)"`
|
LastUsedPasscode string `xorm:"VARCHAR(10)"`
|
||||||
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateScratchToken recreates the scratch token the user is using.
|
// GenerateScratchToken recreates the scratch token the user is using.
|
||||||
func (t *TwoFactor) GenerateScratchToken() error {
|
func (t *TwoFactor) GenerateScratchToken() (string, error) {
|
||||||
token, err := generate.GetRandomString(8)
|
token, err := generate.GetRandomString(8)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
t.ScratchToken = token
|
t.ScratchSalt, _ = generate.GetRandomString(10)
|
||||||
return nil
|
t.ScratchHash = hashToken(token, t.ScratchSalt)
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashToken(token, salt string) string {
|
||||||
|
tempHash := pbkdf2.Key([]byte(token), []byte(salt), 10000, 50, sha256.New)
|
||||||
|
return fmt.Sprintf("%x", tempHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyScratchToken verifies if the specified scratch token is valid.
|
// VerifyScratchToken verifies if the specified scratch token is valid.
|
||||||
|
@ -47,7 +57,8 @@ func (t *TwoFactor) VerifyScratchToken(token string) bool {
|
||||||
if len(token) == 0 {
|
if len(token) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return subtle.ConstantTimeCompare([]byte(token), []byte(t.ScratchToken)) == 1
|
tempHash := hashToken(token, t.ScratchSalt)
|
||||||
|
return subtle.ConstantTimeCompare([]byte(t.ScratchHash), []byte(tempHash)) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TwoFactor) getEncryptionKey() []byte {
|
func (t *TwoFactor) getEncryptionKey() []byte {
|
||||||
|
@ -118,7 +129,7 @@ func aesDecrypt(key, text []byte) ([]byte, error) {
|
||||||
|
|
||||||
// NewTwoFactor creates a new two-factor authentication token.
|
// NewTwoFactor creates a new two-factor authentication token.
|
||||||
func NewTwoFactor(t *TwoFactor) error {
|
func NewTwoFactor(t *TwoFactor) error {
|
||||||
err := t.GenerateScratchToken()
|
_, err := t.GenerateScratchToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ package models
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -23,7 +24,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NonexistentID an ID that will never exist
|
// NonexistentID an ID that will never exist
|
||||||
const NonexistentID = 9223372036854775807
|
const NonexistentID = int64(math.MaxInt64)
|
||||||
|
|
||||||
// giteaRoot a path to the gitea root
|
// giteaRoot a path to the gitea root
|
||||||
var giteaRoot string
|
var giteaRoot string
|
||||||
|
|
|
@ -23,6 +23,7 @@ const (
|
||||||
EnvRepoUsername = "GITEA_REPO_USER_NAME"
|
EnvRepoUsername = "GITEA_REPO_USER_NAME"
|
||||||
EnvRepoIsWiki = "GITEA_REPO_IS_WIKI"
|
EnvRepoIsWiki = "GITEA_REPO_IS_WIKI"
|
||||||
EnvPusherName = "GITEA_PUSHER_NAME"
|
EnvPusherName = "GITEA_PUSHER_NAME"
|
||||||
|
EnvPusherEmail = "GITEA_PUSHER_EMAIL"
|
||||||
EnvPusherID = "GITEA_PUSHER_ID"
|
EnvPusherID = "GITEA_PUSHER_ID"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -83,18 +83,23 @@ type User struct {
|
||||||
Email string `xorm:"NOT NULL"`
|
Email string `xorm:"NOT NULL"`
|
||||||
KeepEmailPrivate bool
|
KeepEmailPrivate bool
|
||||||
Passwd string `xorm:"NOT NULL"`
|
Passwd string `xorm:"NOT NULL"`
|
||||||
LoginType LoginType
|
|
||||||
LoginSource int64 `xorm:"NOT NULL DEFAULT 0"`
|
// MustChangePassword is an attribute that determines if a user
|
||||||
LoginName string
|
// is to change his/her password after registration.
|
||||||
Type UserType
|
MustChangePassword bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
OwnedOrgs []*User `xorm:"-"`
|
|
||||||
Orgs []*User `xorm:"-"`
|
LoginType LoginType
|
||||||
Repos []*Repository `xorm:"-"`
|
LoginSource int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||||
Location string
|
LoginName string
|
||||||
Website string
|
Type UserType
|
||||||
Rands string `xorm:"VARCHAR(10)"`
|
OwnedOrgs []*User `xorm:"-"`
|
||||||
Salt string `xorm:"VARCHAR(10)"`
|
Orgs []*User `xorm:"-"`
|
||||||
Language string `xorm:"VARCHAR(5)"`
|
Repos []*Repository `xorm:"-"`
|
||||||
|
Location string
|
||||||
|
Website string
|
||||||
|
Rands string `xorm:"VARCHAR(10)"`
|
||||||
|
Salt string `xorm:"VARCHAR(10)"`
|
||||||
|
Language string `xorm:"VARCHAR(5)"`
|
||||||
|
|
||||||
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
|
||||||
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
@ -683,7 +688,35 @@ func NewGhostUser() *User {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
reservedUsernames = []string{"assets", "css", "explore", "img", "js", "less", "plugins", "debug", "raw", "install", "api", "avatars", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "error", "new", ".", ".."}
|
reservedUsernames = []string{
|
||||||
|
"admin",
|
||||||
|
"api",
|
||||||
|
"assets",
|
||||||
|
"avatars",
|
||||||
|
"commits",
|
||||||
|
"css",
|
||||||
|
"debug",
|
||||||
|
"error",
|
||||||
|
"explore",
|
||||||
|
"help",
|
||||||
|
"img",
|
||||||
|
"install",
|
||||||
|
"issues",
|
||||||
|
"js",
|
||||||
|
"less",
|
||||||
|
"new",
|
||||||
|
"org",
|
||||||
|
"plugins",
|
||||||
|
"pulls",
|
||||||
|
"raw",
|
||||||
|
"repo",
|
||||||
|
"stars",
|
||||||
|
"template",
|
||||||
|
"user",
|
||||||
|
"vendor",
|
||||||
|
".",
|
||||||
|
"..",
|
||||||
|
}
|
||||||
reservedUserPatterns = []string{"*.keys"}
|
reservedUserPatterns = []string{"*.keys"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -770,8 +803,6 @@ func CreateUser(u *User) (err error) {
|
||||||
|
|
||||||
if _, err = sess.Insert(u); err != nil {
|
if _, err = sess.Insert(u); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if err = os.MkdirAll(UserPath(u.Name), os.ModePerm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
|
@ -870,7 +901,12 @@ func ChangeUserName(u *User, newUserName string) (err error) {
|
||||||
return fmt.Errorf("Delete repository wiki local copy: %v", err)
|
return fmt.Errorf("Delete repository wiki local copy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.Rename(UserPath(u.Name), UserPath(newUserName))
|
// Do not fail if directory does not exist
|
||||||
|
if err = os.Rename(UserPath(u.Name), UserPath(newUserName)); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("Rename user directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDupEmail checks whether there are the same email with the user
|
// checkDupEmail checks whether there are the same email with the user
|
||||||
|
|
|
@ -77,13 +77,13 @@ func TestSearchUsers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1},
|
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1},
|
||||||
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20})
|
[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21})
|
||||||
|
|
||||||
testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse},
|
testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse},
|
||||||
[]int64{9})
|
[]int64{9})
|
||||||
|
|
||||||
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
|
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
|
||||||
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20})
|
[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21})
|
||||||
|
|
||||||
testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
|
testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
|
||||||
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
|
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
|
||||||
|
|
|
@ -12,12 +12,13 @@ import (
|
||||||
|
|
||||||
// AdminCreateUserForm form for admin to create user
|
// AdminCreateUserForm form for admin to create user
|
||||||
type AdminCreateUserForm struct {
|
type AdminCreateUserForm struct {
|
||||||
LoginType string `binding:"Required"`
|
LoginType string `binding:"Required"`
|
||||||
LoginName string
|
LoginName string
|
||||||
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:"MaxSize(255)"`
|
Password string `binding:"MaxSize(255)"`
|
||||||
SendNotify bool
|
SendNotify bool
|
||||||
|
MustChangePassword bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates form fields
|
// Validate validates form fields
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/routers/utils"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
"github.com/go-macaron/binding"
|
"github.com/go-macaron/binding"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
|
@ -113,6 +115,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
|
||||||
|
@ -224,6 +227,11 @@ func (f *NewSlackHookForm) Validate(ctx *macaron.Context, errs binding.Errors) b
|
||||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasInvalidChannel validates the channel name is in the right format
|
||||||
|
func (f NewSlackHookForm) HasInvalidChannel() bool {
|
||||||
|
return !utils.IsValidSlackChannel(f.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
// NewDiscordHookForm form for creating discord hook
|
// NewDiscordHookForm form for creating discord hook
|
||||||
type NewDiscordHookForm struct {
|
type NewDiscordHookForm struct {
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
|
@ -362,6 +370,53 @@ func (f *MergePullRequestForm) Validate(ctx *macaron.Context, errs binding.Error
|
||||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CodeCommentForm form for adding code comments for PRs
|
||||||
|
type CodeCommentForm struct {
|
||||||
|
Content string `binding:"Required"`
|
||||||
|
Side string `binding:"Required;In(previous,proposed)"`
|
||||||
|
Line int64
|
||||||
|
TreePath string `form:"path" binding:"Required"`
|
||||||
|
IsReview bool `form:"is_review"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the fields
|
||||||
|
func (f *CodeCommentForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitReviewForm for submitting a finished code review
|
||||||
|
type SubmitReviewForm struct {
|
||||||
|
Content string
|
||||||
|
Type string `binding:"Required;In(approve,comment,reject)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the fields
|
||||||
|
func (f *SubmitReviewForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReviewType will return the corresponding reviewtype for type
|
||||||
|
func (f SubmitReviewForm) ReviewType() models.ReviewType {
|
||||||
|
switch f.Type {
|
||||||
|
case "approve":
|
||||||
|
return models.ReviewTypeApprove
|
||||||
|
case "comment":
|
||||||
|
return models.ReviewTypeComment
|
||||||
|
case "reject":
|
||||||
|
return models.ReviewTypeReject
|
||||||
|
default:
|
||||||
|
return models.ReviewTypeUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasEmptyContent checks if the content of the review form is empty.
|
||||||
|
func (f SubmitReviewForm) HasEmptyContent() bool {
|
||||||
|
reviewType := f.ReviewType()
|
||||||
|
|
||||||
|
return (reviewType == models.ReviewTypeComment || reviewType == models.ReviewTypeReject) &&
|
||||||
|
len(strings.TrimSpace(f.Content)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
// __________ .__
|
// __________ .__
|
||||||
// \______ \ ____ | | ____ _____ ______ ____
|
// \______ \ ____ | | ____ _____ ______ ____
|
||||||
// | _// __ \| | _/ __ \\__ \ / ___// __ \
|
// | _// __ \| | _/ __ \\__ \ / ___// __ \
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
// 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 auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubmitReviewForm_IsEmpty(t *testing.T) {
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
form SubmitReviewForm
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
// Approved PR with a comment shouldn't count as empty
|
||||||
|
{SubmitReviewForm{Type: "approve", Content: "Awesome"}, false},
|
||||||
|
|
||||||
|
// Approved PR without a comment shouldn't count as empty
|
||||||
|
{SubmitReviewForm{Type: "approve", Content: ""}, false},
|
||||||
|
|
||||||
|
// Rejected PR without a comment should count as empty
|
||||||
|
{SubmitReviewForm{Type: "reject", Content: ""}, true},
|
||||||
|
|
||||||
|
// Rejected PR with a comment shouldn't count as empty
|
||||||
|
{SubmitReviewForm{Type: "reject", Content: "Awesome"}, false},
|
||||||
|
|
||||||
|
// Comment review on a PR with a comment shouldn't count as empty
|
||||||
|
{SubmitReviewForm{Type: "comment", Content: "Awesome"}, false},
|
||||||
|
|
||||||
|
// Comment review on a PR without a comment should count as empty
|
||||||
|
{SubmitReviewForm{Type: "comment", Content: ""}, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range cases {
|
||||||
|
assert.Equal(t, v.expected, v.form.HasEmptyContent())
|
||||||
|
}
|
||||||
|
}
|
|
@ -72,11 +72,12 @@ 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:"MaxSize(255)"`
|
Password string `binding:"Required;MaxSize(255)"`
|
||||||
Retype string
|
Retype string
|
||||||
Remember bool
|
Remember bool
|
||||||
|
GRecaptchaResponse string `form:"g-recaptcha-response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate valideates the fields
|
// Validate valideates the fields
|
||||||
|
@ -84,10 +85,21 @@ func (f *RegisterForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi
|
||||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustChangePasswordForm form for updating your password after account creation
|
||||||
|
// by an admin
|
||||||
|
type MustChangePasswordForm struct {
|
||||||
|
Password string `binding:"Required;MaxSize(255)"`
|
||||||
|
Retype string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate valideates the fields
|
||||||
|
func (f *MustChangePasswordForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
|
}
|
||||||
|
|
||||||
// SignInForm form for signing in with user/password
|
// SignInForm form for signing in with user/password
|
||||||
type SignInForm struct {
|
type SignInForm struct {
|
||||||
UserName string `binding:"Required;MaxSize(254)"`
|
UserName string `binding:"Required;MaxSize(254)"`
|
||||||
// TODO remove required from password for SecondFactorAuthentication
|
|
||||||
Password string `binding:"Required;MaxSize(255)"`
|
Password string `binding:"Required;MaxSize(255)"`
|
||||||
Remember bool
|
Remember bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -31,10 +31,31 @@ func Toggle(options *ToggleOptions) macaron.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check prohibit login users.
|
// Check prohibit login users.
|
||||||
if ctx.IsSigned && ctx.User.ProhibitLogin {
|
if ctx.IsSigned {
|
||||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
|
||||||
ctx.HTML(200, "user/auth/prohibit_login")
|
if ctx.User.ProhibitLogin {
|
||||||
return
|
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||||
|
ctx.HTML(200, "user/auth/prohibit_login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent infinite redirection
|
||||||
|
// also make sure that the form cannot be accessed by
|
||||||
|
// users who don't need this
|
||||||
|
if ctx.Req.URL.Path == setting.AppSubURL+"/user/settings/change_password" {
|
||||||
|
if !ctx.User.MustChangePassword {
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.User.MustChangePassword {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
|
||||||
|
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
|
||||||
|
ctx.SetCookie("redirect_to", url.QueryEscape(setting.AppSubURL+ctx.Req.RequestURI), 0, setting.AppSubURL)
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
|
||||||
|
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.
|
||||||
|
|
|
@ -261,10 +261,15 @@ func Contexter() macaron.Handler {
|
||||||
log.Debug("Session ID: %s", sess.ID())
|
log.Debug("Session ID: %s", sess.ID())
|
||||||
log.Debug("CSRF Token: %v", ctx.Data["CsrfToken"])
|
log.Debug("CSRF Token: %v", ctx.Data["CsrfToken"])
|
||||||
|
|
||||||
|
ctx.Data["IsLandingPageHome"] = setting.LandingPageURL == setting.LandingPageHome
|
||||||
|
ctx.Data["IsLandingPageExplore"] = setting.LandingPageURL == setting.LandingPageExplore
|
||||||
|
ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations
|
||||||
|
|
||||||
ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton
|
ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton
|
||||||
ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding
|
ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding
|
||||||
ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion
|
ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion
|
||||||
ctx.Data["EnableSwaggerEndpoint"] = setting.API.EnableSwaggerEndpoint
|
|
||||||
|
ctx.Data["EnableSwagger"] = setting.API.EnableSwagger
|
||||||
ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn
|
ctx.Data["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn
|
||||||
|
|
||||||
c.Map(ctx)
|
c.Map(ctx)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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><br/></td><tr></table>",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range kases {
|
||||||
|
res := parser.Render([]byte(k), "", nil, false)
|
||||||
|
assert.EqualValues(t, v, string(res))
|
||||||
|
}
|
||||||
|
}
|
|
@ -191,10 +191,10 @@ func PostProcess(
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
// create the context from the parameters
|
// create the context from the parameters
|
||||||
ctx := &postProcessCtx{
|
ctx := &postProcessCtx{
|
||||||
metas: metas,
|
metas: metas,
|
||||||
urlPrefix: urlPrefix,
|
urlPrefix: urlPrefix,
|
||||||
isWikiMarkdown: isWikiMarkdown,
|
isWikiMarkdown: isWikiMarkdown,
|
||||||
procs: defaultProcessors,
|
procs: defaultProcessors,
|
||||||
visitLinksForShortLinks: true,
|
visitLinksForShortLinks: true,
|
||||||
}
|
}
|
||||||
return ctx.postProcess(rawHTML)
|
return ctx.postProcess(rawHTML)
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
// 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 pprof
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DumpMemProfileForUsername dumps a memory profile at pprofDataPath as memprofile_<username>_<temporary id>
|
||||||
|
func DumpMemProfileForUsername(pprofDataPath, username string) {
|
||||||
|
f, err := ioutil.TempFile(pprofDataPath, fmt.Sprintf("memprofile_%s_", username))
|
||||||
|
if err != nil {
|
||||||
|
log.GitLogger.Fatal(4, "Could not create memory profile: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
runtime.GC() // get up-to-date statistics
|
||||||
|
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||||
|
log.GitLogger.Fatal(4, "Could not write memory profile: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpCPUProfileForUsername dumps a CPU profile at pprofDataPath as cpuprofile_<username>_<temporary id>
|
||||||
|
// it returns the stop function which stops, writes and closes the CPU profile file
|
||||||
|
func DumpCPUProfileForUsername(pprofDataPath, username string) func() {
|
||||||
|
f, err := ioutil.TempFile(pprofDataPath, fmt.Sprintf("cpuprofile_%s_", username))
|
||||||
|
if err != nil {
|
||||||
|
log.GitLogger.Fatal(4, "Could not create cpu profile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pprof.StartCPUProfile(f)
|
||||||
|
return func() {
|
||||||
|
pprof.StopCPUProfile()
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// 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 recaptcha
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response is the structure of JSON returned from API
|
||||||
|
type Response struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ChallengeTS time.Time `json:"challenge_ts"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
ErrorCodes []string `json:"error-codes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiURL = "https://www.google.com/recaptcha/api/siteverify"
|
||||||
|
|
||||||
|
// Verify calls Google Recaptcha API to verify token
|
||||||
|
func Verify(response string) (bool, error) {
|
||||||
|
resp, err := http.PostForm(apiURL,
|
||||||
|
url.Values{"secret": {setting.Service.RecaptchaSecret}, "response": {response}})
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Failed to send CAPTCHA response: %s", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Failed to read CAPTCHA response: %s", err)
|
||||||
|
}
|
||||||
|
var jsonResponse Response
|
||||||
|
err = json.Unmarshal(body, &jsonResponse)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Failed to parse CAPTCHA response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse.Success, nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue