Browse Source

merged with gogs

pull/2039/head
Kim Lindhardt Madsen 9 years ago
parent
commit
7252c5e6e0
  1. 4
      .bra.toml
  2. 1
      .gitignore
  3. 58
      .gopmfile
  4. 4
      .travis.yml
  5. 2
      Dockerfile
  6. 35
      Makefile
  7. 49
      README.md
  8. 42
      README_ZH.md
  9. 12
      cmd/cert.go
  10. 42
      cmd/cmd.go
  11. 4
      cmd/dump.go
  12. 93
      cmd/serve.go
  13. 12
      cmd/update.go
  14. 59
      cmd/web.go
  15. 18
      conf/app.ini
  16. 1
      conf/gitignore/Clojure.gitignore
  17. 1
      conf/gitignore/Fortran.gitignore
  18. 45
      conf/locale/TRANSLATORS
  19. 14
      conf/locale/locale_bg-BG.ini
  20. 18
      conf/locale/locale_de-DE.ini
  21. 34
      conf/locale/locale_en-US.ini
  22. 48
      conf/locale/locale_es-ES.ini
  23. 24
      conf/locale/locale_fr-FR.ini
  24. 36
      conf/locale/locale_it-IT.ini
  25. 282
      conf/locale/locale_ja-JP.ini
  26. 504
      conf/locale/locale_lv-LV.ini
  27. 12
      conf/locale/locale_nl-NL.ini
  28. 152
      conf/locale/locale_pl-PL.ini
  29. 194
      conf/locale/locale_pt-BR.ini
  30. 488
      conf/locale/locale_ru-RU.ini
  31. 16
      conf/locale/locale_zh-CN.ini
  32. 16
      conf/locale/locale_zh-HK.ini
  33. 12
      docker-compose.yml
  34. 2
      docker/build.sh
  35. 1
      docker/s6/gogs/setup
  36. 3
      docker/s6/openssh/setup
  37. 7
      docker/s6/syslogd/run
  38. 64
      docker/start.sh
  39. 4
      gogs.go
  40. 28
      models/access.go
  41. 85
      models/action.go
  42. 89
      models/error.go
  43. 40
      models/git_diff.go
  44. 28
      models/issue.go
  45. 53
      models/migrations/migrations.go
  46. 2
      models/models.go
  47. 65
      models/publickey.go
  48. 434
      models/pull.go
  49. 36
      models/release.go
  50. 160
      models/repo.go
  51. 65
      models/update.go
  52. 49
      models/user.go
  53. 82
      models/webhook.go
  54. 21
      modules/auth/admin.go
  55. 14
      modules/auth/auth.go
  56. 44
      modules/auth/ldap/ldap.go
  57. 48
      modules/auth/repo_form.go
  58. 4
      modules/auth/user_form.go
  59. 8
      modules/avatar/avatar.go
  60. 19
      modules/base/base.go
  61. 24
      modules/base/markdown.go
  62. 18
      modules/base/tool.go
  63. 64
      modules/bindata/bindata.go
  64. 2
      modules/cron/parser_test.go
  65. 615
      modules/crypto/ssh/agent/client.go
  66. 287
      modules/crypto/ssh/agent/client_test.go
  67. 103
      modules/crypto/ssh/agent/forward.go
  68. 184
      modules/crypto/ssh/agent/keyring.go
  69. 209
      modules/crypto/ssh/agent/server.go
  70. 77
      modules/crypto/ssh/agent/server_test.go
  71. 64
      modules/crypto/ssh/agent/testdata_test.go
  72. 122
      modules/crypto/ssh/benchmark_test.go
  73. 98
      modules/crypto/ssh/buffer.go
  74. 87
      modules/crypto/ssh/buffer_test.go
  75. 501
      modules/crypto/ssh/certs.go
  76. 216
      modules/crypto/ssh/certs_test.go
  77. 631
      modules/crypto/ssh/channel.go
  78. 549
      modules/crypto/ssh/cipher.go
  79. 127
      modules/crypto/ssh/cipher_test.go
  80. 213
      modules/crypto/ssh/client.go
  81. 441
      modules/crypto/ssh/client_auth.go
  82. 393
      modules/crypto/ssh/client_auth_test.go
  83. 39
      modules/crypto/ssh/client_test.go
  84. 354
      modules/crypto/ssh/common.go
  85. 144
      modules/crypto/ssh/connection.go
  86. 18
      modules/crypto/ssh/doc.go
  87. 211
      modules/crypto/ssh/example_test.go
  88. 412
      modules/crypto/ssh/handshake.go
  89. 415
      modules/crypto/ssh/handshake_test.go
  90. 526
      modules/crypto/ssh/kex.go
  91. 50
      modules/crypto/ssh/kex_test.go
  92. 628
      modules/crypto/ssh/keys.go
  93. 306
      modules/crypto/ssh/keys_test.go
  94. 57
      modules/crypto/ssh/mac.go
  95. 110
      modules/crypto/ssh/mempipe_test.go
  96. 725
      modules/crypto/ssh/messages.go
  97. 254
      modules/crypto/ssh/messages_test.go
  98. 356
      modules/crypto/ssh/mux.go
  99. 525
      modules/crypto/ssh/mux_test.go
  100. 493
      modules/crypto/ssh/server.go
  101. Some files were not shown because too many files have changed in this diff Show More

4
.bra.toml

@ -13,7 +13,7 @@ watch_dirs = [
watch_exts = [".go"] watch_exts = [".go"]
build_delay = 1500 build_delay = 1500
cmds = [ cmds = [
["go", "install", "-tags", "sqlite"],# redis memcache cert pam tidb ["go", "install", "-race"], # sqlite redis memcache cert pam tidb
["go", "build", "-tags", "sqlite"], ["go", "build", "-race"],
["./gogs", "web"] ["./gogs", "web"]
] ]

1
.gitignore vendored

@ -36,3 +36,4 @@ docker/docker/init_gogs.sh
gogs.sublime-project gogs.sublime-project
gogs.sublime-workspace gogs.sublime-workspace
.tags* .tags*
release

58
.gopmfile

@ -3,41 +3,43 @@ path = github.com/gogits/gogs
[deps] [deps]
github.com/bradfitz/gomemcache = commit:72a68649ba github.com/bradfitz/gomemcache = commit:72a68649ba
github.com/Unknwon/cae = commit:2e70a1351b github.com/codegangsta/cli = commit:70e3fa5
github.com/Unknwon/com = commit:47d7d2b81a github.com/go-macaron/binding = commit:864a5ce
github.com/Unknwon/i18n = commit:7457d88830 github.com/go-macaron/cache = commit:5617353
github.com/Unknwon/paginater = commit:cab2d086fa github.com/go-macaron/captcha = commit:875ff77
github.com/codegangsta/cli = commit:142e6cd241 github.com/go-macaron/csrf = commit:75c2b04
github.com/go-macaron/gzip = commit:4938e9b
github.com/go-macaron/i18n = commit:5e728b6
github.com/go-macaron/inject = commit:c5ab7bf
github.com/go-macaron/session = commit:66031fc
github.com/go-macaron/toolbox =
github.com/go-sql-driver/mysql = commit:527bcd55aa github.com/go-sql-driver/mysql = commit:527bcd55aa
github.com/go-xorm/core = commit:3e10003353 github.com/go-xorm/core = commit:3e10003353
github.com/go-xorm/xorm = commit:803f6db50c github.com/go-xorm/xorm = commit:8bf4405
github.com/gogits/chardet = commit:2404f77725 github.com/gogits/chardet = commit:2404f77725
github.com/kiliit/go-gogs-client github.com/kiliit/go-gogs-client
github.com/issue9/identicon = github.com/issue9/identicon = commit:5a61672
github.com/lib/pq = commit:b269bd035a github.com/klauspost/compress = commit:0449b1c
github.com/go-macaron/binding = github.com/klauspost/cpuid = commit:8d9fe96
github.com/go-macaron/cache = github.com/klauspost/crc32 = commit:f8d2e12
github.com/go-macaron/captcha = github.com/lib/pq = commit:83c4f41
github.com/go-macaron/csrf = github.com/mattn/go-sqlite3 = commit:5651a9d
github.com/go-macaron/gzip = github.com/mcuadros/go-version = commit:d52711f
github.com/go-macaron/i18n = github.com/microcosm-cc/bluemonday = commit:4ac6f27
github.com/go-macaron/session =
github.com/go-macaron/toolbox =
github.com/klauspost/compress =
github.com/klauspost/crc32 =
github.com/mattn/go-sqlite3 = commit:b808f01f66
github.com/mcuadros/go-version = commit:d52711f8d6
github.com/microcosm-cc/bluemonday = commit:85ba47ef2c
github.com/mssola/user_agent = commit:a163d6a569
github.com/msteinert/pam = commit:6534f23b39 github.com/msteinert/pam = commit:6534f23b39
github.com/nfnt/resize = commit:dc93e1b98c github.com/nfnt/resize = commit:dc93e1b98c
github.com/russross/blackfriday = commit:8cec3a854e github.com/russross/blackfriday = commit:510be64
github.com/shurcooL/sanitized_anchor_name = commit:244f5ac324 github.com/shurcooL/sanitized_anchor_name = commit:10ef21a
github.com/Unknwon/cae = commit:7f5e046
github.com/Unknwon/com = commit:28b053d
github.com/Unknwon/i18n = commit:7457d88830
github.com/Unknwon/paginater = commit:7748a72
golang.org/x/net = golang.org/x/net =
golang.org/x/text = golang.org/x/text =
gopkg.in/gomail.v2 = commit:b1e55520bf golang.org/x/crypto =
gopkg.in/macaron.v1 = gopkg.in/gomail.v2 = commit:df6fc79
gopkg.in/ini.v1 = commit:e8c222fea7 gopkg.in/ini.v1 = commit:060d7da
gopkg.in/macaron.v1 = commit:1c6dd87
gopkg.in/redis.v2 = commit:e617904962 gopkg.in/redis.v2 = commit:e617904962
[res] [res]

4
.travis.yml

@ -4,13 +4,15 @@ go:
- 1.3 - 1.3
- 1.4 - 1.4
- 1.5 - 1.5
- tip
before_install: before_install:
- sudo apt-get update -qq - sudo apt-get update -qq
- sudo apt-get install -y libpam-dev - sudo apt-get install -y libpam-dev
- go get github.com/msteinert/pam - go get github.com/msteinert/pam
install:
- go get -t -v ./...
script: go build -v -tags "pam" script: go build -v -tags "pam"
notifications: notifications:

2
Dockerfile

@ -19,4 +19,4 @@ RUN ./docker/build.sh
VOLUME ["/data"] VOLUME ["/data"]
EXPOSE 22 3000 EXPOSE 22 3000
ENTRYPOINT ["docker/start.sh"] ENTRYPOINT ["docker/start.sh"]
CMD ["/usr/bin/s6-svscan", "/app/gogs/docker/s6/"] CMD ["/bin/s6-svscan", "/app/gogs/docker/s6/"]

35
Makefile

@ -0,0 +1,35 @@
LDFLAGS += -X "github.com/gogits/gogs/modules/setting.BuildTime=$(shell date -u '+%Y-%m-%d %I:%M:%S %Z')"
LDFLAGS += -X "github.com/gogits/gogs/modules/setting.BuildGitHash=$(shell git rev-parse HEAD)"
TAGS = ""
RELEASE_ROOT = "release"
RELEASE_GOGS = "release/gogs"
NOW = $(shell date -u '+%Y%m%d%I%M%S')
.PHONY: build pack release bindata clean
build:
go install -ldflags '$(LDFLAGS)' -tags '$(TAGS)'
go build -ldflags '$(LDFLAGS)' -tags '$(TAGS)'
govet:
go tool vet -composites=false -methods=false -structtags=false .
pack:
rm -rf $(RELEASE_GOGS)
mkdir -p $(RELEASE_GOGS)
cp -r gogs LICENSE README.md README_ZH.md templates public scripts $(RELEASE_GOGS)
rm -rf $(RELEASE_GOGS)/public/config.codekit $(RELEASE_GOGS)/public/less
cd $(RELEASE_ROOT) && zip -r gogs.$(NOW).zip "gogs"
release: build pack
bindata:
go-bindata -o=modules/bindata/bindata.go -ignore="\\.DS_Store|README.md" -pkg=bindata conf/...
clean:
go clean -i ./...
clean-mac: clean
find . -name ".DS_Store" -print0 | xargs -0 rm

49
README.md

@ -5,7 +5,7 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra
![](public/img/gogs-large-resize.png) ![](public/img/gogs-large-resize.png)
##### Current version: 0.6.16 Beta ##### Current version: 0.7.16 Beta
<table> <table>
<tr> <tr>
@ -29,20 +29,20 @@ Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?bra
- Due to testing purpose, data of [try.gogs.io](https://try.gogs.io) has been reset in **Jan 28, 2015** and will reset multiple times after. Please do **NOT** put your important data on the site. - Due to testing purpose, data of [try.gogs.io](https://try.gogs.io) has been reset in **Jan 28, 2015** and will reset multiple times after. Please do **NOT** put your important data on the site.
- The demo site [try.gogs.io](https://try.gogs.io) is running under `develop` branch. - The demo site [try.gogs.io](https://try.gogs.io) is running under `develop` branch.
- :exclamation::exclamation::exclamation:<span style="color: red">You **MUST** read [CONTRIBUTING.md](CONTRIBUTING.md) before you start filing an issue or making a Pull Request, and **MUST** discuss with us on [Gitter](https://gitter.im/gogits/gogs) for UI changes and feature Pull Requests, otherwise it's high possibilities that we are not going to merge it.</span>:exclamation::exclamation::exclamation: - :bangbang:<span style="color: red">You **MUST** read [CONTRIBUTING.md](CONTRIBUTING.md) before you start filing an issue or making a Pull Request, and **MUST** discuss with us on [Gitter](https://gitter.im/gogits/gogs) for UI changes and feature Pull Requests, otherwise it's high possibilities that we are not going to merge it.</span>:bangbang:
- If you think there are vulnerabilities in the project, please talk privately to **u@gogs.io**. Thanks! - If you think there are vulnerabilities in the project, please talk privately to **u@gogs.io**. Thanks!
- If you're interested in using APIs, we have experimental support with [documentation](https://github.com/gogits/go-gogs-client/wiki).
- If your team/company is using Gogs and would like to put your logo on [our website](http://gogs.io), contact us by any means.
#### Other language version [简体中文](README_ZH.md)
- [简体中文](README_ZH.md)
## Purpose ## Purpose
The goal of this project is to make the easiest, fastest, and most painless way of setting up a self-hosted Git service. With Go, this can be done with an independent binary distribution across **ALL platforms** that Go supports, including Linux, Mac OS X, and Windows. The goal of this project is to make the easiest, fastest, and most painless way of setting up a self-hosted Git service. With Go, this can be done with an independent binary distribution across **ALL platforms** that Go supports, including Linux, Mac OS X, Windows and ARM.
## Overview ## Overview
- Please see the [Documentation](http://gogs.io/docs/intro/) for project design, known issues, and change log. - Please see the [Documentation](http://gogs.io/docs/intro) for common usages and change log.
- See the [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) to follow the develop team. - See the [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) to follow the develop team.
- Want to try it before doing anything else? Do it [online](https://try.gogs.io/gogs/gogs) or go down to the **Installation -> Install from binary** section! - Want to try it before doing anything else? Do it [online](https://try.gogs.io/gogs/gogs) or go down to the **Installation -> Install from binary** section!
- Having trouble? Get help with [Troubleshooting](http://gogs.io/docs/intro/troubleshooting.html). - Having trouble? Get help with [Troubleshooting](http://gogs.io/docs/intro/troubleshooting.html).
@ -51,24 +51,25 @@ The goal of this project is to make the easiest, fastest, and most painless way
## Features ## Features
- Activity timeline - Activity timeline
- SSH/HTTP(S) protocol support - SSH and HTTP/HTTPS protocols
- SMTP/LDAP/reverse proxy authentication support - SMTP/LDAP/Reverse proxy authentication
- Reverse proxy suburl support - Reverse proxy with sub-path
- Account/Organization(with team)/Repository management - Account/Organization/Repository management
- Repository/Organization webhooks(including Slack) - Repository/Organization webhooks (including Slack)
- Repository Git hooks/deploy keys - Repository Git hooks/deploy keys
- Add/remove repository collaborators - Repository issues and pull requests
- Gravatar and custom source support - Add/Remove repository collaborators
- Gravatar and custom source
- Mail service - Mail service
- Administration panel - Administration panel
- CI integration: [Drone](https://github.com/drone/drone) - CI integration: [Drone](https://github.com/drone/drone)
- Supports MySQL, PostgreSQL, SQLite3 and [TiDB](https://github.com/pingcap/tidb) - Supports MySQL, PostgreSQL, SQLite3 and [TiDB](https://github.com/pingcap/tidb) (experimental)
- Multi-language support ([14 languages](https://crowdin.com/project/gogs)) - Multi-language support ([14 languages](https://crowdin.com/project/gogs))
## System Requirements ## System Requirements
- A cheap Raspberry Pi is powerful enough for basic functionality. - A cheap Raspberry Pi is powerful enough for basic functionality.
- At least 2 CPU cores and 1GB RAM would be the baseline for teamwork. - 2 CPU cores and 1GB RAM would be the baseline for teamwork.
## Browser Support ## Browser Support
@ -77,7 +78,7 @@ The goal of this project is to make the easiest, fastest, and most painless way
## Installation ## Installation
Make sure you install the [prerequisites](http://gogs.io/docs/installation/) first. Make sure you install the [prerequisites](http://gogs.io/docs/installation) first.
There are 5 ways to install Gogs: There are 5 ways to install Gogs:
@ -91,6 +92,7 @@ There are 5 ways to install Gogs:
- [How To Set Up Gogs on Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-gogs-on-ubuntu-14-04) - [How To Set Up Gogs on Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-set-up-gogs-on-ubuntu-14-04)
- [Run your own GitHub-like service with the help of Docker](http://blog.hypriot.com/post/run-your-own-github-like-service-with-docker/) - [Run your own GitHub-like service with the help of Docker](http://blog.hypriot.com/post/run-your-own-github-like-service-with-docker/)
- [使用 Gogs 搭建自己的 Git 服务器](https://mynook.info/blog/post/host-your-own-git-server-using-gogs) (Chinese)
- [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654) (Chinese) - [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654) (Chinese)
- [Installing Gogs on FreeBSD](https://www.codejam.info/2015/03/installing-gogs-on-freebsd.html) - [Installing Gogs on FreeBSD](https://www.codejam.info/2015/03/installing-gogs-on-freebsd.html)
- [Gogs on Raspberry Pi](http://blog.meinside.pe.kr/Gogs-on-Raspberry-Pi/) - [Gogs on Raspberry Pi](http://blog.meinside.pe.kr/Gogs-on-Raspberry-Pi/)
@ -105,18 +107,25 @@ There are 5 ways to install Gogs:
- [Cloudron](https://cloudron.io/appstore.html#io.gogs.cloudronapp) - [Cloudron](https://cloudron.io/appstore.html#io.gogs.cloudronapp)
- [Scaleway](https://www.scaleway.com/imagehub/gogs/) - [Scaleway](https://www.scaleway.com/imagehub/gogs/)
- [Portal](https://portaldemo.xyz/cloud/) - [Portal](https://portaldemo.xyz/cloud/)
- [Sandstorm](https://github.com/cem/gogs-sandstorm)
### Product Support
- [Synology](https://www.synology.com) (Docker)
- [One Space](http://www.onespace.cc) (App Store)
## Acknowledgments ## Acknowledgments
- Router and middleware mechanism of [Macaron](https://github.com/Unknwon/macaron). - Router and middleware mechanism of [Macaron](https://github.com/go-macaron/macaron).
- Mail Service, modules design is inspired by [WeTalk](https://github.com/beego/wetalk). - Modules design is inspired by [WeTalk](https://github.com/beego/wetalk).
- System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog). - System Monitor Status is inspired by [GoBlog](https://github.com/fuxiaohei/goblog).
- Thanks [lavachen](http://www.lavachen.cn/) and [Rocker](http://weibo.com/rocker1989) for designing Logo. - Thanks [lavachen](http://www.lavachen.cn/) and [Rocker](http://weibo.com/rocker1989) for designing Logo.
- Thanks [Crowdin](https://crowdin.com/project/gogs) for providing open source translation plan. - Thanks [Crowdin](https://crowdin.com/project/gogs) for providing open source translation plan.
- Thanks [DigitalOcean](https://www.digitalocean.com) for hosting home and demo sites.
## Contributors ## Contributors
- Ex-team members [@lunny](https://github.com/lunny) and [@fuxiaohei](https://github.com/fuxiaohei). - Ex-team members [@lunny](https://github.com/lunny), [@fuxiaohei](https://github.com/fuxiaohei) and [@slene](https://github.com/slene).
- See [contributors page](https://github.com/gogits/gogs/graphs/contributors) for full list of contributors. - See [contributors page](https://github.com/gogits/gogs/graphs/contributors) for full list of contributors.
- See [TRANSLATORS](conf/locale/TRANSLATORS) for public list of translators. - See [TRANSLATORS](conf/locale/TRANSLATORS) for public list of translators.

42
README_ZH.md

@ -1,15 +1,15 @@
Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?branch=master)](https://travis-ci.org/gogits/gogs) Gogs - Go Git Service [![Build Status](https://travis-ci.org/gogits/gogs.svg?branch=master)](https://travis-ci.org/gogits/gogs)
===================== =====================
Gogs (Go Git Service) 是一款可轻易搭建的自助 Git 服务。 Gogs (Go Git Service) 是一款易搭建的自助 Git 服务。
## 开发目的 ## 开发目的
Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自助 Git 服务。使用 Go 语言开发使得 Gogs 能够通过独立的二进制分发,并且支持 Go 语言支持的 **所有平台**,包括 Linux、Mac OS X 以及 Windows Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自助 Git 服务。使用 Go 语言开发使得 Gogs 能够通过独立的二进制分发,并且支持 Go 语言支持的 **所有平台**,包括 Linux、Mac OS X、Windows 以及 ARM 平台
## 项目概览 ## 项目概览
- 有关项目设计、已知问题和变更日志,请通过 [使用手册](http://gogs.io/docs/intro/) 查看。 - 有关基本用法和变更日志,请通过 [使用手册](http://gogs.io/docs/intro/) 查看。
- 您可以到 [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) 跟随开发团队的脚步。 - 您可以到 [Trello Board](https://trello.com/b/uxAoeLUl/gogs-go-git-service) 跟随开发团队的脚步。
- 想要先睹为快?通过 [在线体验](https://try.gogs.io/gogs/gogs) 或查看 **安装部署 -> 二进制安装** 小节。 - 想要先睹为快?通过 [在线体验](https://try.gogs.io/gogs/gogs) 或查看 **安装部署 -> 二进制安装** 小节。
- 使用过程中遇到问题?尝试从 [故障排查](http://gogs.io/docs/intro/troubleshooting.html) 页面获取帮助。 - 使用过程中遇到问题?尝试从 [故障排查](http://gogs.io/docs/intro/troubleshooting.html) 页面获取帮助。
@ -18,18 +18,19 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
## 功能特性 ## 功能特性
- 支持活动时间线 - 支持活动时间线
- 支持 SSH/HTTP(S) 协议 - 支持 SSH 以及 HTTP/HTTPS 协议
- 支持 SMTP/LDAP/反向代理的用户认证 - 支持 SMTP、LDAP 和反向代理的用户认证
- 支持反向代理子路径 - 支持反向代理子路径
- 支持用户、组织和仓库管理系统 - 支持用户、组织和仓库管理系统
- 支持仓库和组织级别 Web 钩子(包括 Slack 集成) - 支持仓库和组织级别 Web 钩子(包括 Slack 集成)
- 支持仓库 Git 钩子和部署密钥 - 支持仓库 Git 钩子和部署密钥
- 支持 添加/删除 仓库协作者 - 支持仓库工单(Issue)和合并请求(Pull Request)
- 支持添加和删除仓库协作者
- 支持 Gravatar 以及自定义源 - 支持 Gravatar 以及自定义源
- 支持邮件服务 - 支持邮件服务
- 支持后台管理面板 - 支持后台管理面板
- 支持 CI 集成:[Drone](https://github.com/drone/drone) - 支持 CI 集成:[Drone](https://github.com/drone/drone)
- 支持 MySQL、PostgreSQL、SQLite3 和 [TiDB](https://github.com/pingcap/tidb) 数据库 - 支持 MySQL、PostgreSQL、SQLite3 和 [TiDB](https://github.com/pingcap/tidb)(实验性支持) 数据库
- 支持多语言本地化([14 种语言]([more](https://crowdin.com/project/gogs))) - 支持多语言本地化([14 种语言]([more](https://crowdin.com/project/gogs)))
## 系统要求 ## 系统要求
@ -44,7 +45,7 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
## 安装部署 ## 安装部署
在安装 Gogs 之前,您需要先安装 [基本环境](http://gogs.io/docs/installation/)。 在安装 Gogs 之前,您需要先安装 [基本环境](http://gogs.io/docs/installation)。
然后,您可以通过以下 5 种方式来安装 Gogs: 然后,您可以通过以下 5 种方式来安装 Gogs:
@ -54,17 +55,36 @@ Gogs 的目标是打造一个最简单、最快速和最轻松的方式搭建自
- [采用 Docker 部署](https://github.com/gogits/gogs/tree/master/docker) - [采用 Docker 部署](https://github.com/gogits/gogs/tree/master/docker)
- [通过 Vagrant 安装](https://github.com/geerlingguy/ansible-vagrant-examples/tree/master/gogs) - [通过 Vagrant 安装](https://github.com/geerlingguy/ansible-vagrant-examples/tree/master/gogs)
### 使用教程
- [使用 Gogs 搭建自己的 Git 服务器](https://mynook.info/blog/post/host-your-own-git-server-using-gogs)
- [阿里云上 Ubuntu 14.04 64 位安装 Gogs](http://my.oschina.net/luyao/blog/375654)
### 云端部署
- [OpenShift](https://github.com/tkisme/gogs-openshift)
- [Cloudron](https://cloudron.io/appstore.html#io.gogs.cloudronapp)
- [Scaleway](https://www.scaleway.com/imagehub/gogs/)
- [Portal](https://portaldemo.xyz/cloud/)
- [Sandstorm](https://github.com/cem/gogs-sandstorm)
### 产品支持
- [Synology](https://www.synology.com)(Docker)
- [One Space](http://www.onespace.cc)(应用商店)
## 特别鸣谢 ## 特别鸣谢
- 基于 [Macaron](https://github.com/Unknwon/macaron) 的路由与中间件机制。 - 基于 [Macaron](https://github.com/go-macaron/macaron) 的路由与中间件机制。
- 基于 [WeTalk](https://github.com/beego/wetalk) 修改的邮件服务和模块设计。 - 基于 [WeTalk](https://github.com/beego/wetalk) 修改的模块设计。
- 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。 - 基于 [GoBlog](https://github.com/fuxiaohei/goblog) 修改的系统监视状态。
- 感谢 [lavachen](http://www.lavachen.cn/) 和 [Rocker](http://weibo.com/rocker1989) 设计的 Logo。 - 感谢 [lavachen](http://www.lavachen.cn/) 和 [Rocker](http://weibo.com/rocker1989) 设计的 Logo。
- 感谢 [Crowdin](https://crowdin.com/project/gogs) 提供免费的开源项目本地化支持。 - 感谢 [Crowdin](https://crowdin.com/project/gogs) 提供免费的开源项目本地化支持。
- 感谢 [DigitalOcean](https://www.digitalocean.com) 提供主站和体验站点的服务器赞助。
## 贡献成员 ## 贡献成员
- 前团队成员 [@lunny](https://github.com/lunny) 和 [@fuxiaohei](https://github.com/fuxiaohei)。 - 前团队成员 [@lunny](https://github.com/lunny)、[@fuxiaohei](https://github.com/fuxiaohei) 和 [@slene](https://github.com/slene)。
- 您可以通过查看 [贡献者页面](https://github.com/gogits/gogs/graphs/contributors) 获取完整的贡献者列表。 - 您可以通过查看 [贡献者页面](https://github.com/gogits/gogs/graphs/contributors) 获取完整的贡献者列表。
- 您可以通过查看 [TRANSLATORS](conf/locale/TRANSLATORS) 文件获取公开的翻译人员列表。 - 您可以通过查看 [TRANSLATORS](conf/locale/TRANSLATORS) 文件获取公开的翻译人员列表。

12
cmd/cert.go

@ -32,12 +32,12 @@ var CmdCert = cli.Command{
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`, Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
Action: runCert, Action: runCert,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"host", "", "Comma-separated hostnames and IPs to generate a certificate for", ""}, stringFlag("host", "", "Comma-separated hostnames and IPs to generate a certificate for"),
cli.StringFlag{"ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521", ""}, stringFlag("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521"),
cli.IntFlag{"rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set", ""}, intFlag("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set"),
cli.StringFlag{"start-date", "", "Creation date formatted as Jan 1 15:04:05 2011", ""}, stringFlag("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011"),
cli.DurationFlag{"duration", 365 * 24 * time.Hour, "Duration that certificate is valid for", ""}, durationFlag("duration", 365*24*time.Hour, "Duration that certificate is valid for"),
cli.BoolFlag{"ca", "whether this cert should be its own Certificate Authority", ""}, boolFlag("ca", "whether this cert should be its own Certificate Authority"),
}, },
} }

42
cmd/cmd.go

@ -0,0 +1,42 @@
// 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 cmd
import (
"time"
"github.com/codegangsta/cli"
)
func stringFlag(name, value, usage string) cli.StringFlag {
return cli.StringFlag{
Name: name,
Value: value,
Usage: usage,
}
}
func boolFlag(name, usage string) cli.BoolFlag {
return cli.BoolFlag{
Name: name,
Usage: usage,
}
}
func intFlag(name string, value int, usage string) cli.IntFlag {
return cli.IntFlag{
Name: name,
Value: value,
Usage: usage,
}
}
func durationFlag(name string, value time.Duration, usage string) cli.DurationFlag {
return cli.DurationFlag{
Name: name,
Value: value,
Usage: usage,
}
}

4
cmd/dump.go

@ -25,8 +25,8 @@ var CmdDump = cli.Command{
It can be used for backup and capture Gogs server image to send to maintainer`, It can be used for backup and capture Gogs server image to send to maintainer`,
Action: runDump, Action: runDump,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"config, c", "custom/conf/app.ini", "Custom configuration file path", ""}, stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
cli.BoolFlag{"verbose, v", "show process details", ""}, boolFlag("verbose, v", "show process details"),
}, },
} }

93
cmd/serve.go

@ -5,6 +5,7 @@
package cmd package cmd
import ( import (
"crypto/tls"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -32,7 +33,7 @@ var CmdServ = cli.Command{
Description: `Serv provide access auth for repositories`, Description: `Serv provide access auth for repositories`,
Action: runServ, Action: runServ,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"config, c", "custom/conf/app.ini", "Custom configuration file path", ""}, stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
}, },
} }
@ -73,7 +74,51 @@ var (
func fail(userMessage, logMessage string, args ...interface{}) { func fail(userMessage, logMessage string, args ...interface{}) {
fmt.Fprintln(os.Stderr, "Gogs:", userMessage) fmt.Fprintln(os.Stderr, "Gogs:", userMessage)
log.GitLogger.Fatal(3, logMessage, args...)
if len(logMessage) > 0 {
log.GitLogger.Fatal(3, logMessage, args...)
return
}
log.GitLogger.Close()
os.Exit(1)
}
func handleUpdateTask(uuid string, user *models.User, repoUserName, repoName string) {
task, err := models.GetUpdateTaskByUUID(uuid)
if err != nil {
if models.IsErrUpdateTaskNotExist(err) {
log.GitLogger.Trace("No update task is presented: %s", uuid)
return
}
log.GitLogger.Fatal(2, "GetUpdateTaskByUUID: %v", err)
}
if err = models.Update(task.RefName, task.OldCommitID, task.NewCommitID,
user.Name, repoUserName, repoName, user.Id); err != nil {
log.GitLogger.Error(2, "Update: %v", err)
}
if err = models.DeleteUpdateTaskByUUID(uuid); err != nil {
log.GitLogger.Fatal(2, "DeleteUpdateTaskByUUID: %v", err)
}
// Ask for running deliver hook and test pull request tasks.
reqURL := setting.AppUrl + repoUserName + "/" + repoName + "/tasks/trigger?branch=" +
strings.TrimPrefix(task.RefName, "refs/heads/")
log.GitLogger.Trace("Trigger task: %s", reqURL)
resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
InsecureSkipVerify: true,
}).Response()
if err == nil {
resp.Body.Close()
if resp.StatusCode/100 != 2 {
log.GitLogger.Error(2, "Fail to trigger task: not 2xx response code")
}
} else {
log.GitLogger.Error(2, "Fail to trigger task: %v", err)
}
} }
func runServ(c *cli.Context) { func runServ(c *cli.Context) {
@ -94,13 +139,13 @@ func runServ(c *cli.Context) {
} }
verb, args := parseCmd(cmd) verb, args := parseCmd(cmd)
repoPath := strings.Trim(args, "'") repoPath := strings.ToLower(strings.Trim(args, "'"))
rr := strings.SplitN(repoPath, "/", 2) rr := strings.SplitN(repoPath, "/", 2)
if len(rr) != 2 { if len(rr) != 2 {
fail("Invalid repository path", "Invalid repository path: %v", args) fail("Invalid repository path", "Invalid repository path: %v", args)
} }
repoUserName := rr[0] repoUserName := strings.ToLower(rr[0])
repoName := strings.TrimSuffix(rr[1], ".git") repoName := strings.ToLower(strings.TrimSuffix(rr[1], ".git"))
repoUser, err := models.GetUserByName(repoUserName) repoUser, err := models.GetUserByName(repoUserName)
if err != nil { if err != nil {
@ -123,6 +168,11 @@ func runServ(c *cli.Context) {
fail("Unknown git command", "Unknown git command %s", verb) fail("Unknown git command", "Unknown git command %s", verb)
} }
// Prohibit push to mirror repositories.
if requestedMode > models.ACCESS_MODE_READ && repo.IsMirror {
fail("mirror repository is read-only", "")
}
// Allow anonymous clone for public repositories. // Allow anonymous clone for public repositories.
var ( var (
keyID int64 keyID int64
@ -131,12 +181,12 @@ func runServ(c *cli.Context) {
if requestedMode == models.ACCESS_MODE_WRITE || repo.IsPrivate { if requestedMode == models.ACCESS_MODE_WRITE || repo.IsPrivate {
keys := strings.Split(c.Args()[0], "-") keys := strings.Split(c.Args()[0], "-")
if len(keys) != 2 { if len(keys) != 2 {
fail("Key ID format error", "Invalid key ID: %s", c.Args()[0]) fail("Key ID format error", "Invalid key argument: %s", c.Args()[0])
} }
key, err := models.GetPublicKeyByID(com.StrTo(keys[1]).MustInt64()) key, err := models.GetPublicKeyByID(com.StrTo(keys[1]).MustInt64())
if err != nil { if err != nil {
fail("Key ID format error", "Invalid key ID[%s]: %v", c.Args()[0], err) fail("Invalid key ID", "Invalid key ID[%s]: %v", c.Args()[0], err)
} }
keyID = key.ID keyID = key.ID
@ -161,7 +211,7 @@ func runServ(c *cli.Context) {
fail("Internal error", "UpdateDeployKey: %v", err) fail("Internal error", "UpdateDeployKey: %v", err)
} }
} else { } else {
user, err = models.GetUserByKeyId(key.ID) user, err = models.GetUserByKeyID(key.ID)
if err != nil { if err != nil {
fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err) fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err)
} }
@ -200,32 +250,7 @@ func runServ(c *cli.Context) {
} }
if requestedMode == models.ACCESS_MODE_WRITE { if requestedMode == models.ACCESS_MODE_WRITE {
tasks, err := models.GetUpdateTasksByUuid(uuid) handleUpdateTask(uuid, user, repoUserName, repoName)
if err != nil {
log.GitLogger.Fatal(2, "GetUpdateTasksByUuid: %v", err)
}
for _, task := range tasks {
err = models.Update(task.RefName, task.OldCommitId, task.NewCommitId,
user.Name, repoUserName, repoName, user.Id)
if err != nil {
log.GitLogger.Error(2, "Failed to update: %v", err)
}
}
if err = models.DelUpdateTasksByUuid(uuid); err != nil {
log.GitLogger.Fatal(2, "DelUpdateTasksByUuid: %v", err)
}
}
// Send deliver hook request.
reqURL := setting.AppUrl + repoUserName + "/" + repoName + "/hooks/trigger"
resp, err := httplib.Head(reqURL).Response()
if err == nil {
resp.Body.Close()
log.GitLogger.Trace("Trigger hook: %s", reqURL)
} else {
log.GitLogger.Error(2, "Fail to trigger hook: %v", err)
} }
// Update user key activity. // Update user key activity.

12
cmd/update.go

@ -20,7 +20,7 @@ var CmdUpdate = cli.Command{
Description: `Update get pushed info and insert into database`, Description: `Update get pushed info and insert into database`,
Action: runUpdate, Action: runUpdate,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"config, c", "custom/conf/app.ini", "Custom configuration file path", ""}, stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
}, },
} }
@ -42,16 +42,14 @@ func runUpdate(c *cli.Context) {
log.GitLogger.Fatal(2, "refName is empty, shouldn't use") log.GitLogger.Fatal(2, "refName is empty, shouldn't use")
} }
uuid := os.Getenv("uuid")
task := models.UpdateTask{ task := models.UpdateTask{
Uuid: uuid, UUID: os.Getenv("uuid"),
RefName: args[0], RefName: args[0],
OldCommitId: args[1], OldCommitID: args[1],
NewCommitId: args[2], NewCommitID: args[2],
} }
if err := models.AddUpdateTask(&task); err != nil { if err := models.AddUpdateTask(&task); err != nil {
log.GitLogger.Fatal(2, err.Error()) log.GitLogger.Fatal(2, "AddUpdateTask: %v", err)
} }
} }

59
cmd/web.go

@ -7,7 +7,7 @@ package cmd
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"html/template" gotmpl "html/template"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/fcgi" "net/http/fcgi"
@ -35,11 +35,11 @@ import (
"github.com/gogits/gogs/modules/auth" "github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/auth/apiv1" "github.com/gogits/gogs/modules/auth/apiv1"
"github.com/gogits/gogs/modules/avatar" "github.com/gogits/gogs/modules/avatar"
"github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/bindata" "github.com/gogits/gogs/modules/bindata"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/middleware"
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
"github.com/gogits/gogs/modules/template"
"github.com/gogits/gogs/routers" "github.com/gogits/gogs/routers"
"github.com/gogits/gogs/routers/admin" "github.com/gogits/gogs/routers/admin"
"github.com/gogits/gogs/routers/api/v1" "github.com/gogits/gogs/routers/api/v1"
@ -56,8 +56,8 @@ var CmdWeb = cli.Command{
and it takes care of all the other things for you`, and it takes care of all the other things for you`,
Action: runWeb, Action: runWeb,
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{"port, p", "3000", "Temporary port number to prevent conflict", ""}, stringFlag("port, p", "3000", "Temporary port number to prevent conflict"),
cli.StringFlag{"config, c", "custom/conf/app.ini", "Custom configuration file path", ""}, stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
}, },
} }
@ -80,13 +80,14 @@ func checkVersion() {
// Check dependency version. // Check dependency version.
checkers := []VerChecker{ checkers := []VerChecker{
{"github.com/go-xorm/xorm", func() string { return xorm.Version }, "0.4.3.0806"}, {"github.com/go-xorm/xorm", func() string { return xorm.Version }, "0.4.4.1029"},
{"github.com/Unknwon/macaron", macaron.Version, "0.5.4"}, {"github.com/Unknwon/macaron", macaron.Version, "0.5.4"},
{"github.com/macaron-contrib/binding", binding.Version, "0.1.0"}, {"github.com/go-macaron/binding", binding.Version, "0.1.0"},
{"github.com/macaron-contrib/cache", cache.Version, "0.1.2"}, {"github.com/go-macaron/cache", cache.Version, "0.1.2"},
{"github.com/macaron-contrib/csrf", csrf.Version, "0.0.3"}, {"github.com/go-macaron/csrf", csrf.Version, "0.0.3"},
{"github.com/macaron-contrib/i18n", i18n.Version, "0.0.7"}, {"github.com/go-macaron/i18n", i18n.Version, "0.0.7"},
{"github.com/macaron-contrib/session", session.Version, "0.1.6"}, {"github.com/go-macaron/session", session.Version, "0.1.6"},
{"github.com/go-macaron/toolbox", toolbox.Version, "0.1.0"},
{"gopkg.in/ini.v1", ini.Version, "1.3.4"}, {"gopkg.in/ini.v1", ini.Version, "1.3.4"},
} }
for _, c := range checkers { for _, c := range checkers {
@ -124,7 +125,7 @@ func newMacaron() *macaron.Macaron {
)) ))
m.Use(macaron.Renderer(macaron.RenderOptions{ m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: path.Join(setting.StaticRootPath, "templates"), Directory: path.Join(setting.StaticRootPath, "templates"),
Funcs: []template.FuncMap{base.TemplateFuncs}, Funcs: []gotmpl.FuncMap{template.Funcs},
IndentJSON: macaron.Env != macaron.PROD, IndentJSON: macaron.Env != macaron.PROD,
})) }))
@ -226,14 +227,14 @@ func runWeb(ctx *cli.Context) {
m.Group("/repos", func() { m.Group("/repos", func() {
m.Get("/search", v1.SearchRepos) m.Get("/search", v1.SearchRepos)
})
m.Group("", func() { m.Group("/repos", func() {
m.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), v1.MigrateRepo) m.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), v1.MigrateRepo)
m.Delete("/:username/:reponame", v1.DeleteRepo) m.Combo("/:username/:reponame").Get(v1.GetRepo).
}, middleware.ApiReqToken()) Delete(v1.DeleteRepo)
m.Group("/:username/:reponame", func() { m.Group("/:username/:reponame", func() {
m.Get("", v1.GetRepo)
m.Combo("/hooks").Get(v1.ListRepoHooks). m.Combo("/hooks").Get(v1.ListRepoHooks).
Post(bind(api.CreateHookOption{}), v1.CreateRepoHook) Post(bind(api.CreateHookOption{}), v1.CreateRepoHook)
@ -253,8 +254,8 @@ func runWeb(ctx *cli.Context) {
Post(bindIgnErr(api.CreateReleaseOption{}), v1.CreateRelease) Post(bindIgnErr(api.CreateReleaseOption{}), v1.CreateRelease)
m.Get("/:release", v1.ReleaseByName) m.Get("/:release", v1.ReleaseByName)
}, middleware.RepoRef()) }, middleware.RepoRef())
}, middleware.ApiRepoAssignment(), middleware.ApiReqToken()) }, middleware.ApiRepoAssignment())
}) }, middleware.ApiReqToken())
m.Any("/*", func(ctx *middleware.Context) { m.Any("/*", func(ctx *middleware.Context) {
ctx.Error(404) ctx.Error(404)
@ -478,8 +479,10 @@ func runWeb(ctx *cli.Context) {
m.Post("/delete", repo.DeleteDeployKey) m.Post("/delete", repo.DeleteDeployKey)
}) })
}, func(ctx *middleware.Context) {
ctx.Data["PageIsSettings"] = true
}) })
}, reqSignIn, middleware.RepoAssignment(true), reqRepoAdmin) }, reqSignIn, middleware.RepoAssignment(true), reqRepoAdmin, middleware.RepoRef())
m.Group("/:username/:reponame", func() { m.Group("/:username/:reponame", func() {
m.Get("/action/:action", repo.Action) m.Get("/action/:action", repo.Action)
@ -527,11 +530,17 @@ func runWeb(ctx *cli.Context) {
}, reqSignIn, middleware.RepoAssignment(true)) }, reqSignIn, middleware.RepoAssignment(true))
m.Group("/:username/:reponame", func() { m.Group("/:username/:reponame", func() {
m.Get("/releases", middleware.RepoRef(), repo.Releases) m.Group("", func() {
m.Get("/^:type(issues|pulls)$", repo.RetrieveLabels, repo.Issues) m.Get("/releases", repo.Releases)
m.Get("/^:type(issues|pulls)$", repo.RetrieveLabels, repo.Issues)
m.Get("/labels/", repo.RetrieveLabels, repo.Labels)
m.Get("/milestones", repo.Milestones)
}, middleware.RepoRef(),
func(ctx *middleware.Context) {
ctx.Data["PageIsList"] = true
})
m.Get("/^:type(issues|pulls)$/:index", repo.ViewIssue) m.Get("/^:type(issues|pulls)$/:index", repo.ViewIssue)
m.Get("/labels/", repo.RetrieveLabels, repo.Labels)
m.Get("/milestones", repo.Milestones)
m.Get("/branches", repo.Branches) m.Get("/branches", repo.Branches)
m.Get("/archive/*", repo.Download) m.Get("/archive/*", repo.Download)
@ -561,8 +570,8 @@ func runWeb(ctx *cli.Context) {
}, ignSignIn, middleware.RepoAssignment(true, true), middleware.RepoRef()) }, ignSignIn, middleware.RepoAssignment(true, true), middleware.RepoRef())
m.Group("/:reponame", func() { m.Group("/:reponame", func() {
m.Any("/*", ignSignInAndCsrf, repo.Http) m.Any("/*", ignSignInAndCsrf, repo.HTTP)
m.Head("/hooks/trigger", repo.TriggerHook) m.Head("/tasks/trigger", repo.TriggerTask)
}) })
}) })
// ***** END: Repository ***** // ***** END: Repository *****

18
conf/app.ini

@ -11,6 +11,12 @@ RUN_MODE = dev
[repository] [repository]
ROOT = ROOT =
SCRIPT_TYPE = bash SCRIPT_TYPE = bash
; Default ANSI charset
ANSI_CHARSET =
; Force every new repository to be private
FORCE_PRIVATE = false
; Patch test queue length, make it as large as possible
PULL_REQUEST_QUEUE_LENGTH = 10000
[ui] [ui]
; Number of repositories that are showed in one explore page ; Number of repositories that are showed in one explore page
@ -42,6 +48,8 @@ HTTP_ADDR =
HTTP_PORT = 3000 HTTP_PORT = 3000
; Disable SSH feature when not available ; Disable SSH feature when not available
DISABLE_SSH = false DISABLE_SSH = false
; Whether use builtin SSH server or not.
START_SSH_SERVER = false
SSH_PORT = 22 SSH_PORT = 22
; Disable CDN even in "prod" mode ; Disable CDN even in "prod" mode
OFFLINE_MODE = false OFFLINE_MODE = false
@ -110,6 +118,16 @@ DISABLE_MINIMUM_KEY_SIZE_CHECK = false
; Enable captcha validation for registration ; Enable captcha validation for registration
ENABLE_CAPTCHA = true ENABLE_CAPTCHA = true
; used to filter keys which are too short
[service.minimum_key_sizes]
ED25519 = 256
ECDSA = 256
NTRU = 1087
MCE = 1702
McE = 1702
RSA = 1024
DSA = 1024
[webhook] [webhook]
; Hook task queue length ; Hook task queue length
QUEUE_LENGTH = 1000 QUEUE_LENGTH = 1000

1
conf/gitignore/Clojure.gitignore vendored

@ -1 +0,0 @@
Leiningen.gitignore

1
conf/gitignore/Fortran.gitignore vendored

@ -1 +0,0 @@
C++.gitignore

45
conf/locale/TRANSLATORS

@ -1,20 +1,29 @@
# This file lists all PUBLIC individuals having contributed content to the translation. # This file lists all PUBLIC individuals having contributed content to the translation.
# Order of name is meaningless. # Entries are in alphabetical order.
Akihiro YAGASAKI <yaggytter@momiage.com> Akihiro YAGASAKI <yaggytter AT momiage DOT com>
Alexander Steinhöfer <kontakt@lx-s.de> Alexander Steinhöfer <kontakt AT lx-s DOT de>
Alexandre Magno <alexandre.mbm@gmail.com> Alexandre Magno <alexandre DOT mbm AT gmail DOT com>
Barış Arda Yılmaz <ardayilmazgamer@gmail.com> Andrey Nering <andrey AT nering DOT com DOT br>
Christoph Kisfeld <christoph.kisfeld@gmail.com> Arthur Aslanyan <arthur DOT e DOT aslanyan AT gmail DOT com>
Daniel Speichert <daniel@speichert.pl> Barış Arda Yılmaz <ardayilmazgamer AT gmail DOT com>
Gregor Santner <gdev@live.de> Christoph Kisfeld <christoph DOT kisfeld AT gmail DOT com>
Huimin Wang <wanghm2009@hotmail.co.jp> Daniel Speichert <daniel AT speichert DOT pl>
ilko <email> Dmitriy Nogay <me AT catwhocode DOT ga>
Thomas Fanninger <gogs.thomas@fanninger.at> Gregor Santner <gdev AT live DOT de>
Łukasz Jan Niemier <lukasz@niemier.pl> Hamid Feizabadi <hamidfzm AT gmail DOT com>
Lafriks <lafriks@gmail.com> Huimin Wang <wanghm2009 AT hotmail DOT co DOT jp>
Luc Stepniewski <luc@stepniewski.fr> ilko
Miguel de la Cruz <miguel@mcrx.me> Lafriks <lafriks AT gmail DOT com>
Marc Schiller <marc@schiller.im> Lauri Ojansivu <x AT xet7 DOT org>
Morten Sørensen <klim8d@gmail.com> Luc Stepniewski <luc AT stepniewski DOT fr>
Natan Albuquerque <natanalbuquerque5@gmail.com> Marc Schiller <marc AT schiller DOT im>
Miguel de la Cruz <miguel AT mcrx DOT me>
Morten Sørensen <klim8d AT gmail DOT com>
Natan Albuquerque <natanalbuquerque5 AT gmail DOT com>
Odilon Junior <odilon DOT junior93 AT gmail DOT com>
Thomas Fanninger <gogs DOT thomas AT fanninger DOT at>
Tilmann Bach <tilmann AT outlook DOT com>
Vladimir Vissoultchev <wqweto AT gmail DOT com>
YJSoft <yjsoft AT yjsoft DOT pe DOT kr>
Łukasz Jan Niemier <lukasz AT niemier DOT pl>

14
conf/locale/locale_bg-BG.ini

@ -148,7 +148,6 @@ forgot_password=Забравена парола
forget_password=Забравена парола? forget_password=Забравена парола?
sign_up_now=Нуждаете се от профил? Регистрирайте се сега. sign_up_now=Нуждаете се от профил? Регистрирайте се сега.
confirmation_mail_sent_prompt=Ново писмо за потвърждение е изпратено до <b>%s</b>. Моля проверете пощенската си кутия в рамките на следващите %d часа, за да завършите процеса на регистрация. confirmation_mail_sent_prompt=Ново писмо за потвърждение е изпратено до <b>%s</b>. Моля проверете пощенската си кутия в рамките на следващите %d часа, за да завършите процеса на регистрация.
sign_in_to_account=Влезте с Вашия профил
active_your_account=Активиране на профил active_your_account=Активиране на профил
resent_limit_prompt=За съжаление Вие съвсем наскоро изпратихте писмо за активация. Моля изчакайте 3 минути, след което опитайте отново. resent_limit_prompt=За съжаление Вие съвсем наскоро изпратихте писмо за активация. Моля изчакайте 3 минути, след което опитайте отново.
has_unconfirmed_mail=Здравейте %s, имате непотвърден адрес на ел. поща (<b>%s</b>). Ако не сте получили писмо за потвърждение или имате нужда да се изпрати ново писмо, моля щракнете бутона по-долу. has_unconfirmed_mail=Здравейте %s, имате непотвърден адрес на ел. поща (<b>%s</b>). Ако не сте получили писмо за потвърждение или имате нужда да се изпрати ново писмо, моля щракнете бутона по-долу.
@ -192,6 +191,7 @@ min_size_error=` трябва да съдържа поне %s знака.`
max_size_error=` трябва да съдържа най-много %s знака.` max_size_error=` трябва да съдържа най-много %s знака.`
email_error=` не е валиден адрес на ел. поща.` email_error=` не е валиден адрес на ел. поща.`
url_error=` не е валиден URL адрес.` url_error=` не е валиден URL адрес.`
include_error=` трябва да съдържа текст '%s'.`
unknown_error=Неизвестна грешка: unknown_error=Неизвестна грешка:
captcha_incorrect=Captcha не е потвърдена. captcha_incorrect=Captcha не е потвърдена.
password_not_match=Паролата и потвърждението ѝ не съвпадат. password_not_match=Паролата и потвърждението ѝ не съвпадат.
@ -334,6 +334,7 @@ repo_name=Име на хранилището
repo_name_helper=Добро име на хранилище е име, състоящо от кратки, запомнящи се и уникални ключови думи. repo_name_helper=Добро име на хранилище е име, състоящо от кратки, запомнящи се и уникални ключови думи.
visibility=Видимост visibility=Видимост
visiblity_helper=Това хранилище е <span class="ui red text">Частно</span> visiblity_helper=Това хранилище е <span class="ui red text">Частно</span>
visiblity_helper_forced=Административна настройка задължава всички нови хранилища да бъдат <span class="ui red text">Частни</span>
visiblity_fork_helper=(Промяна на тази стойност ще се отрази на всички разклонения) visiblity_fork_helper=(Промяна на тази стойност ще се отрази на всички разклонения)
fork_repo=Разклони хранилището fork_repo=Разклони хранилището
fork_from=Разклонение от fork_from=Разклонение от
@ -359,6 +360,7 @@ migrate_type_helper=Това хранилище ще бъде <span class="text
migrate_repo=Мигрирай хранилище migrate_repo=Мигрирай хранилище
migrate.clone_address=Адрес за клонирай migrate.clone_address=Адрес за клонирай
migrate.clone_address_desc=Това може да е HTTP/HTTPS/GIT адрес или локален път на сървъра. migrate.clone_address_desc=Това може да е HTTP/HTTPS/GIT адрес или локален път на сървъра.
migrate.permission_denied=Недостатъчни права за импорт на локални хранилища.
migrate.invalid_local_path=Невалиден път - не съществува или не е директория. migrate.invalid_local_path=Невалиден път - не съществува или не е директория.
forked_from=разклонено от forked_from=разклонено от
@ -454,9 +456,9 @@ issues.num_comments=%d коментара
issues.commented_at=`коментира <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at=`коментира <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=Все още няма съдържание. issues.no_content=Все още няма съдържание.
issues.close_issue=Затвори issues.close_issue=Затвори
issues.close_comment_issue=Затвори и коментирай issues.close_comment_issue=Kоментирай и затвори
issues.reopen_issue=Отвори повторно issues.reopen_issue=Отвори повторно
issues.reopen_comment_issue=Отвори повторно и коментирай issues.reopen_comment_issue=Kоментирай и oтвори отново
issues.create_comment=Коментирай issues.create_comment=Коментирай
issues.closed_at=`затвори <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`затвори <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`повторно отвори <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`повторно отвори <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -498,10 +500,12 @@ pulls.reopen_to_merge=Моля повторно отворете тази зая
pulls.merged=Обединени pulls.merged=Обединени
pulls.has_merged=Тази заявка за сливане е обединена успешно! pulls.has_merged=Тази заявка за сливане е обединена успешно!
pulls.data_broken=Данните от тази заявка за сливане са невалидни поради изтрита информация за някое разклонение. pulls.data_broken=Данните от тази заявка за сливане са невалидни поради изтрита информация за някое разклонение.
pulls.is_checking=Проверката за конфликт все още е в ход. Моля обновете страницата след малко.
pulls.can_auto_merge_desc=Можете да извършвате авто-обединяване на тази заявка за сливане. pulls.can_auto_merge_desc=Можете да извършвате авто-обединяване на тази заявка за сливане.
pulls.cannot_auto_merge_desc=Не можете да извършите авто-обединяване, защото съществуват конфликти между ревизиите. pulls.cannot_auto_merge_desc=Не можете да извършите авто-обединяване, защото съществуват конфликти между ревизиите.
pulls.cannot_auto_merge_helper=Моля, използвайте инструменти на командния ред за да отстраните проблема. pulls.cannot_auto_merge_helper=Моля, използвайте инструменти на командния ред за да отстраните проблема.
pulls.merge_pull_request=Обедини заявка за сливане pulls.merge_pull_request=Обедини заявка за сливане
pulls.open_unmerged_pull_exists=`Невъзможно повторно отваряне, защото вече съществува заявка за сливане (#%d) от същото хранилище със същата информация за обединяване, която чака да бъде извършена`
milestones.new=Нов етап milestones.new=Нов етап
milestones.open_tab=%d отворени milestones.open_tab=%d отворени
@ -648,6 +652,7 @@ release.tag_name_already_exist=Издание с това име на марке
[org] [org]
org_name_holder=Име на организацията org_name_holder=Име на организацията
org_full_name_holder=Пълно име на организацията
org_name_helper=Добрите имена на организация са кратки и запомнящи се. org_name_helper=Добрите имена на организация са кратки и запомнящи се.
create_org=Създай организация create_org=Създай организация
repo_updated=Обновено repo_updated=Обновено
@ -804,6 +809,7 @@ users.edit_account=Редактирай профил
users.is_activated=Този профил е активиран users.is_activated=Този профил е активиран
users.is_admin=Този профил има административни права users.is_admin=Този профил има административни права
users.allow_git_hook=Този профил има разрешение да създава Git куки users.allow_git_hook=Този профил има разрешение да създава Git куки
users.allow_import_local=Този профил има права за импорт на локални хранилища
users.update_profile=Обнови профила users.update_profile=Обнови профила
users.delete_account=Изтрий този профил users.delete_account=Изтрий този профил
users.still_own_repo=Този профил притежава поне едно хранилище. Първо трябва да изтриете хранилището или да го прехвърлите на друг потребител. users.still_own_repo=Този профил притежава поне едно хранилище. Първо трябва да изтриете хранилището или да го прехвърлите на друг потребител.
@ -835,7 +841,7 @@ auths.domain=Домейн
auths.host=Сървър auths.host=Сървър
auths.port=Порт auths.port=Порт
auths.bind_dn=Име (DN) за свръзка auths.bind_dn=Име (DN) за свръзка
auths.bind_password=Парола за свързка auths.bind_password=Парола за свръзка
auths.bind_password_helper=Внимание: Тази парола се запазва некриптирана. Моля използвайте потребител, който няма административен достъп. auths.bind_password_helper=Внимание: Тази парола се запазва некриптирана. Моля използвайте потребител, който няма административен достъп.
auths.user_base=База с потребители auths.user_base=База с потребители
auths.user_dn=Име (DN) на потребител auths.user_dn=Име (DN) на потребител

18
conf/locale/locale_de-DE.ini

@ -111,7 +111,7 @@ admin_title=Konto-Einstellungen für den Administrator
admin_name=Benutzername admin_name=Benutzername
admin_password=Passwort admin_password=Passwort
confirm_password=Passwort bestätigen confirm_password=Passwort bestätigen
admin_email=E-Mail admin_email=Administrator E-Mail
install_gogs=Gogs installieren install_gogs=Gogs installieren
test_git_failed=Fehler beim Test des 'git' Kommandos: %v test_git_failed=Fehler beim Test des 'git' Kommandos: %v
sqlite3_not_available=Ihre Gogs-Version unterstützt kein SQLite3, bitte lade dir die offizielle binäre Version von %s herunter, NICHT die gobuild-Version. sqlite3_not_available=Ihre Gogs-Version unterstützt kein SQLite3, bitte lade dir die offizielle binäre Version von %s herunter, NICHT die gobuild-Version.
@ -148,7 +148,6 @@ forgot_password=Passwort vergessen
forget_password=Passwort vergessen? forget_password=Passwort vergessen?
sign_up_now=Du willst ein Konto? Jetzt registrieren! sign_up_now=Du willst ein Konto? Jetzt registrieren!
confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Kontrolliere dein Postfach innerhalb der nächsten %d Stunden, um die Registrierung abzuschließen. confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b> gesendet. Kontrolliere dein Postfach innerhalb der nächsten %d Stunden, um die Registrierung abzuschließen.
sign_in_to_account=Mit deinem Konto anmelden
active_your_account=Aktiviere dein Konto active_your_account=Aktiviere dein Konto
resent_limit_prompt=Es tut uns leid, du sendest zu häufig Aktivierungs-E-Mails. Bitte warte 3 Minuten. resent_limit_prompt=Es tut uns leid, du sendest zu häufig Aktivierungs-E-Mails. Bitte warte 3 Minuten.
has_unconfirmed_mail=Hallo %s, du hast eine unbestätigte E-Mail-Adresse (<b>%s</b>). Wenn du keine Bestätigungs-E-Mail erhalten hast oder eine neue benötigst, klicke bitte auf den folgenden Button. has_unconfirmed_mail=Hallo %s, du hast eine unbestätigte E-Mail-Adresse (<b>%s</b>). Wenn du keine Bestätigungs-E-Mail erhalten hast oder eine neue benötigst, klicke bitte auf den folgenden Button.
@ -192,6 +191,7 @@ min_size_error=` muss mindestens %s Zeichen enthalten.`
max_size_error=` darf höchstens %s Zeichen enthalten.` max_size_error=` darf höchstens %s Zeichen enthalten.`
email_error=` ist keine gültige E-Mail-Adresse.` email_error=` ist keine gültige E-Mail-Adresse.`
url_error=` ist keine gültige URL.` url_error=` ist keine gültige URL.`
include_error=`muss den Substring ‚%s‘ enthalten.`
unknown_error=Unbekannter Fehler: unknown_error=Unbekannter Fehler:
captcha_incorrect=Captcha stimmt nicht überein. captcha_incorrect=Captcha stimmt nicht überein.
password_not_match=Die Passwörter stimmen nicht überein. password_not_match=Die Passwörter stimmen nicht überein.
@ -334,6 +334,7 @@ repo_name=Repository-Name
repo_name_helper=Gute Repository-Namen sind kurz, einprägsam und <strong>einzigartig</strong>. repo_name_helper=Gute Repository-Namen sind kurz, einprägsam und <strong>einzigartig</strong>.
visibility=Sichtbarkeit visibility=Sichtbarkeit
visiblity_helper=Diese Repository ist <span class="ui red text">Privat</span> visiblity_helper=Diese Repository ist <span class="ui red text">Privat</span>
visiblity_helper_forced=Der Administrator hat festgelegt, dass alle neuen Repositories <span class="ui red text">privat</span> sein müssen
visiblity_fork_helper=(Eine Änderung dieses Wertes wirkt sich auf alle Forks aus) visiblity_fork_helper=(Eine Änderung dieses Wertes wirkt sich auf alle Forks aus)
fork_repo=Repository abspalten fork_repo=Repository abspalten
fork_from=Forken von fork_from=Forken von
@ -359,6 +360,7 @@ migrate_type_helper=Diese Repository wird ein <span class="text blue">Spiegel</s
migrate_repo=Repository migrieren migrate_repo=Repository migrieren
migrate.clone_address=Adresse kopieren migrate.clone_address=Adresse kopieren
migrate.clone_address_desc=Dies kann eine HTTP/HTTPS/GIT URL oder ein lokaler Serverpfad sein. migrate.clone_address_desc=Dies kann eine HTTP/HTTPS/GIT URL oder ein lokaler Serverpfad sein.
migrate.permission_denied=Ihnen fehlen die Rechte zum Importieren lokaler Repositorys.
migrate.invalid_local_path=Lokaler Pfad ist ungültig, er existiert nicht oder ist kein Ordner. migrate.invalid_local_path=Lokaler Pfad ist ungültig, er existiert nicht oder ist kein Ordner.
forked_from=Geforkt von forked_from=Geforkt von
@ -454,9 +456,9 @@ issues.num_comments=%d Kommentare
issues.commented_at=`kommentiert in <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at=`kommentiert in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=Hier gibt es bis jetzt noch keinen Inhalt. issues.no_content=Hier gibt es bis jetzt noch keinen Inhalt.
issues.close_issue=Schließen issues.close_issue=Schließen
issues.close_comment_issue=Schließen und kommentieren issues.close_comment_issue=Kommentieren und schließen
issues.reopen_issue=Wiedereröffnen issues.reopen_issue=Wiedereröffnen
issues.reopen_comment_issue=Wiedereröffnen und kommentieren issues.reopen_comment_issue=Kommentieren und wiedereröffnen
issues.create_comment=Kommentieren issues.create_comment=Kommentieren
issues.closed_at=`geschlossen in <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`geschlossen in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`wiedereröffnet in <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`wiedereröffnet in <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -498,10 +500,12 @@ pulls.reopen_to_merge=Bitte diese Pull-Anforderung wiedereröffnen, um die Merge
pulls.merged=Zusammengeführt pulls.merged=Zusammengeführt
pulls.has_merged=Dieser Pull-Request wurde erfolgreich zusammengeführt! pulls.has_merged=Dieser Pull-Request wurde erfolgreich zusammengeführt!
pulls.data_broken=Die Daten dieser Pull-Anforderung sind defekt aufgrund des Löschens von Fork-Informationen. pulls.data_broken=Die Daten dieser Pull-Anforderung sind defekt aufgrund des Löschens von Fork-Informationen.
pulls.is_checking=Die Konfliktprüfung ist in Arbeit. Bitte aktualisiere die Seite in wenigen Momenten.
pulls.can_auto_merge_desc=Du kannst eine Auto-Merge Operation auf diese Pull-Anforderung durchführen. pulls.can_auto_merge_desc=Du kannst eine Auto-Merge Operation auf diese Pull-Anforderung durchführen.
pulls.cannot_auto_merge_desc=Es kann keine Auto-Merge Operation durchgeführt werden, da es Konflikte zwischen den Commits gibt. pulls.cannot_auto_merge_desc=Es kann keine Auto-Merge Operation durchgeführt werden, da es Konflikte zwischen den Commits gibt.
pulls.cannot_auto_merge_helper=Bitte benutze ein Kommandozeilentool, um den Konflikt zu lösen. pulls.cannot_auto_merge_helper=Bitte benutze ein Kommandozeilentool, um den Konflikt zu lösen.
pulls.merge_pull_request=Pull-Request zusammenführen pulls.merge_pull_request=Pull-Request zusammenführen
pulls.open_unmerged_pull_exists=`Du kannst die Pull-Anforderung nicht wiedereröffnen, da bereits eine offene Pull-Anforderung (#%d) aus dem selben Repository mit den gleichen Merge-Informationen existiert und auf das Merging wartet.`
milestones.new=Neuer Meilenstein milestones.new=Neuer Meilenstein
milestones.open_tab=%d offen milestones.open_tab=%d offen
@ -516,7 +520,7 @@ milestones.title=Titel
milestones.desc=Beschreibung milestones.desc=Beschreibung
milestones.due_date=Fälligkeitsdatum (optional) milestones.due_date=Fälligkeitsdatum (optional)
milestones.clear=Bereinigen milestones.clear=Bereinigen
milestones.invalid_due_date_format=Format des Fälligkeitsdatums ist ungültig. Es muss das Format 'Jahr-mm-dd' haben. milestones.invalid_due_date_format=Format des Fälligkeitsdatums ist ungültig. Es muss das Format 'JJJJ-mm-dd' haben.
milestones.create_success=Meilenstein '%s' wurde erfolgreich erstellt! milestones.create_success=Meilenstein '%s' wurde erfolgreich erstellt!
milestones.edit=Meilenstein bearbeiten milestones.edit=Meilenstein bearbeiten
milestones.edit_subheader=Benutze eine bessere Beschreibung für die Meilensteine, um die Menschen nicht zu verwirren. milestones.edit_subheader=Benutze eine bessere Beschreibung für die Meilensteine, um die Menschen nicht zu verwirren.
@ -648,6 +652,7 @@ release.tag_name_already_exist=Ein Release mit diesem Tag existiert bereits.
[org] [org]
org_name_holder=Name der Organisation org_name_holder=Name der Organisation
org_full_name_holder=Vollständiger Name der Organisation
org_name_helper=Gute Namen von Organisationen sind kurz und einprägsam. org_name_helper=Gute Namen von Organisationen sind kurz und einprägsam.
create_org=Organisation erstellen create_org=Organisation erstellen
repo_updated=Aktualisiert repo_updated=Aktualisiert
@ -682,7 +687,7 @@ settings.delete_prompt=Die Organisation wird dauerhaft gelöscht. Dies kann <str
settings.confirm_delete_account=Löschen settings.confirm_delete_account=Löschen
settings.delete_org_title=Organisation löschen settings.delete_org_title=Organisation löschen
settings.delete_org_desc=Diese Organisation wird dauerhaft gelöscht, möchtest du fortfahren? settings.delete_org_desc=Diese Organisation wird dauerhaft gelöscht, möchtest du fortfahren?
settings.hooks_desc=Füge Webhooks hinzu, die für <strong>alle</strong> Repositorys dieser Organisation ausgelöst werden. settings.hooks_desc=Füge Webhooks hinzu, die für <strong>alle</strong> Repositories dieser Organisation ausgelöst werden.
members.public=Öffentlich members.public=Öffentlich
members.public_helper=Privat machen members.public_helper=Privat machen
@ -804,6 +809,7 @@ users.edit_account=Konto bearbeiten
users.is_activated=Dieses Konto ist aktiviert users.is_activated=Dieses Konto ist aktiviert
users.is_admin=Dieses Konto hat Administratorrechte users.is_admin=Dieses Konto hat Administratorrechte
users.allow_git_hook=Dieses Konto ist berechtigt, Git-Hooks zu erstellen users.allow_git_hook=Dieses Konto ist berechtigt, Git-Hooks zu erstellen
users.allow_import_local=Dieses Konto ist berechtigt, lokale Repositorys zu importieren
users.update_profile=Kontoprofil aktualisieren users.update_profile=Kontoprofil aktualisieren
users.delete_account=Dieses Konto löschen users.delete_account=Dieses Konto löschen
users.still_own_repo=Dieses Konto besitzt noch Repositories. Diese müssen zuerst gelöscht oder übertragen werden. users.still_own_repo=Dieses Konto besitzt noch Repositories. Diese müssen zuerst gelöscht oder übertragen werden.

34
conf/locale/locale_en-US.ini

@ -111,7 +111,7 @@ admin_title = Admin Account Settings
admin_name = Username admin_name = Username
admin_password = Password admin_password = Password
confirm_password = Confirm Password confirm_password = Confirm Password
admin_email = E-mail admin_email = Admin E-mail
install_gogs = Install Gogs install_gogs = Install Gogs
test_git_failed = Fail to test 'git' command: %v test_git_failed = Fail to test 'git' command: %v
sqlite3_not_available = Your release version does not support SQLite3, please download the official binary version from %s, NOT the gobuild version. sqlite3_not_available = Your release version does not support SQLite3, please download the official binary version from %s, NOT the gobuild version.
@ -148,7 +148,6 @@ forgot_password= Forgot Password
forget_password = Forgot password? forget_password = Forgot password?
sign_up_now = Need an account? Sign up now. sign_up_now = Need an account? Sign up now.
confirmation_mail_sent_prompt = A new confirmation e-mail has been sent to <b>%s</b>, please check your inbox within the next %d hours to complete the registration process. confirmation_mail_sent_prompt = A new confirmation e-mail has been sent to <b>%s</b>, please check your inbox within the next %d hours to complete the registration process.
sign_in_to_account = Sign in to your account
active_your_account = Activate Your Account active_your_account = Activate Your Account
resent_limit_prompt = Sorry, you already requested an activation email recently. Please wait 3 minutes then try again. resent_limit_prompt = Sorry, you already requested an activation email recently. Please wait 3 minutes then try again.
has_unconfirmed_mail = Hi %s, you have an unconfirmed e-mail address (<b>%s</b>). If you haven't received a confirmation e-mail or need to resend a new one, please click on the button below. has_unconfirmed_mail = Hi %s, you have an unconfirmed e-mail address (<b>%s</b>). If you haven't received a confirmation e-mail or need to resend a new one, please click on the button below.
@ -192,6 +191,7 @@ min_size_error = ` must contain at least %s characters.`
max_size_error = ` must contain at most %s characters.` max_size_error = ` must contain at most %s characters.`
email_error = ` is not a valid e-mail address.` email_error = ` is not a valid e-mail address.`
url_error = ` is not a valid URL.` url_error = ` is not a valid URL.`
include_error = ` must contain substring '%s'.`
unknown_error = Unknown error: unknown_error = Unknown error:
captcha_incorrect = Captcha didn't match. captcha_incorrect = Captcha didn't match.
password_not_match = Password and confirm password are not same. password_not_match = Password and confirm password are not same.
@ -334,6 +334,7 @@ repo_name = Repository Name
repo_name_helper = A good repository name is usually composed of short, memorable and unique keywords. repo_name_helper = A good repository name is usually composed of short, memorable and unique keywords.
visibility = Visibility visibility = Visibility
visiblity_helper = This repository is <span class="ui red text">Private</span> visiblity_helper = This repository is <span class="ui red text">Private</span>
visiblity_helper_forced = Site admin has forced all new repositories to be <span class="ui red text">Private</span>
visiblity_fork_helper = (Change of this value will affect all forks) visiblity_fork_helper = (Change of this value will affect all forks)
fork_repo = Fork Repository fork_repo = Fork Repository
fork_from = Fork From fork_from = Fork From
@ -349,6 +350,9 @@ auto_init = Initialize this repository with selected files and template
create_repo = Create Repository create_repo = Create Repository
default_branch = Default Branch default_branch = Default Branch
mirror_interval = Mirror Interval (hour) mirror_interval = Mirror Interval (hour)
watchers = Watchers
stargazers = Stargazers
forks = Forks
form.name_reserved = Repository name '%s' is reserved. form.name_reserved = Repository name '%s' is reserved.
form.name_pattern_not_allowed = Repository name pattern '%s' is not allowed. form.name_pattern_not_allowed = Repository name pattern '%s' is not allowed.
@ -359,6 +363,7 @@ migrate_type_helper = This repository will be a <span class="text blue">mirror</
migrate_repo = Migrate Repository migrate_repo = Migrate Repository
migrate.clone_address = Clone Address migrate.clone_address = Clone Address
migrate.clone_address_desc = This can be a HTTP/HTTPS/GIT URL or local server path. migrate.clone_address_desc = This can be a HTTP/HTTPS/GIT URL or local server path.
migrate.permission_denied = You are not allowed to import local repositories.
migrate.invalid_local_path = Invalid local path, it does not exist or not a directory. migrate.invalid_local_path = Invalid local path, it does not exist or not a directory.
forked_from = forked from forked_from = forked from
@ -366,9 +371,7 @@ fork_from_self = You cannot fork repository you already owned!
copy_link = Copy copy_link = Copy
copy_link_success = Copied! copy_link_success = Copied!
copy_link_error = Press ⌘-C or Ctrl-C to copy copy_link_error = Press ⌘-C or Ctrl-C to copy
click_to_copy = Copy to clipboard
copied = Copied OK copied = Copied OK
clone_helper = Need help cloning? Visit <a target="_blank" href="%s">Help</a>!
unwatch = Unwatch unwatch = Unwatch
watch = Watch watch = Watch
unstar = Unstar unstar = Unstar
@ -382,10 +385,9 @@ create_new_repo_command = Create a new repository on the command line
push_exist_repo = Push an existing repository from the command line push_exist_repo = Push an existing repository from the command line
repo_is_empty = This repository is empty, please come back later! repo_is_empty = This repository is empty, please come back later!
branch = Branch branch = Branch
tree = Tree tree = Tree
branch_and_tags = Branches & Tags filter_branch_and_tag = Filter branch or tag
branches = Branches branches = Branches
tags = Tags tags = Tags
issues = Issues issues = Issues
@ -454,9 +456,9 @@ issues.num_comments = %d comments
issues.commented_at = `commented <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at = `commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content = There is no content yet. issues.no_content = There is no content yet.
issues.close_issue = Close issues.close_issue = Close
issues.close_comment_issue = Close and comment issues.close_comment_issue = Comment and close
issues.reopen_issue = Reopen issues.reopen_issue = Reopen
issues.reopen_comment_issue = Reopen and comment issues.reopen_comment_issue = Comment and reopen
issues.create_comment = Comment issues.create_comment = Comment
issues.closed_at = `closed <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at = `closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at = `reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at = `reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -480,6 +482,7 @@ issues.label_deletion = Label Deletion
issues.label_deletion_desc = Delete this label will remove its information in all related issues. Do you want to continue? issues.label_deletion_desc = Delete this label will remove its information in all related issues. Do you want to continue?
issues.label_deletion_success = Label has been deleted successfully! issues.label_deletion_success = Label has been deleted successfully!
pulls.new = New Pull Request
pulls.compare_changes = Compare Changes pulls.compare_changes = Compare Changes
pulls.compare_changes_desc = Compare two branches and make a pull request for changes. pulls.compare_changes_desc = Compare two branches and make a pull request for changes.
pulls.compare_base = base pulls.compare_base = base
@ -498,6 +501,7 @@ pulls.reopen_to_merge = Please reopen this pull request to perform merge operati
pulls.merged = Merged pulls.merged = Merged
pulls.has_merged = This pull request has been merged successfully! pulls.has_merged = This pull request has been merged successfully!
pulls.data_broken = Data of this pull request has been broken due to deletion of fork information. pulls.data_broken = Data of this pull request has been broken due to deletion of fork information.
pulls.is_checking = The conflict checking is still in progress, please refresh page in few moments.
pulls.can_auto_merge_desc = You can perform auto-merge operation on this pull request. pulls.can_auto_merge_desc = You can perform auto-merge operation on this pull request.
pulls.cannot_auto_merge_desc = You can't perform auto-merge operation because there are conflicts between commits. pulls.cannot_auto_merge_desc = You can't perform auto-merge operation because there are conflicts between commits.
pulls.cannot_auto_merge_helper = Please use command line tool to solve it. pulls.cannot_auto_merge_helper = Please use command line tool to solve it.
@ -517,7 +521,7 @@ milestones.title = Title
milestones.desc = Description milestones.desc = Description
milestones.due_date = Due Date (optional) milestones.due_date = Due Date (optional)
milestones.clear = Clear milestones.clear = Clear
milestones.invalid_due_date_format = Due date format is invalid, must be 'year-mm-dd'. milestones.invalid_due_date_format = Due date format is invalid, must be 'yyyy-mm-dd'.
milestones.create_success = Milestone '%s' has been created successfully! milestones.create_success = Milestone '%s' has been created successfully!
milestones.edit = Edit Milestone milestones.edit = Edit Milestone
milestones.edit_subheader = Use better description for milestones so people won't be confused. milestones.edit_subheader = Use better description for milestones so people won't be confused.
@ -559,6 +563,7 @@ settings.confirm_delete = Confirm Deletion
settings.add_collaborator = Add New Collaborator settings.add_collaborator = Add New Collaborator
settings.add_collaborator_success = New collaborator has been added. settings.add_collaborator_success = New collaborator has been added.
settings.remove_collaborator_success = Collaborator has been removed. settings.remove_collaborator_success = Collaborator has been removed.
settings.search_user_placeholder = Search user...
settings.user_is_org_member = User is organization member who cannot be added as a collaborator. settings.user_is_org_member = User is organization member who cannot be added as a collaborator.
settings.add_webhook = Add Webhook settings.add_webhook = Add Webhook
settings.hooks_desc = Webhooks are much like basic HTTP POST event triggers. Whenever something occurs in Gogs, we will handle the notification to the target host you specify. Learn more in this <a target="_blank" href="%s">Webhooks Guide</a>. settings.hooks_desc = Webhooks are much like basic HTTP POST event triggers. Whenever something occurs in Gogs, we will handle the notification to the target host you specify. Learn more in this <a target="_blank" href="%s">Webhooks Guide</a>.
@ -631,24 +636,28 @@ release.stable = Stable
release.edit = edit release.edit = edit
release.ahead = <strong>%d</strong> commits to %s since this release release.ahead = <strong>%d</strong> commits to %s since this release
release.source_code = Source Code release.source_code = Source Code
release.new_subheader = Publish releases to iterate product.
release.edit_subheader = Detailed change log can help users understand what has been improved.
release.tag_name = Tag name release.tag_name = Tag name
release.target = Target release.target = Target
release.tag_helper = Choose an existing tag, or create a new tag on publish. release.tag_helper = Choose an existing tag, or create a new tag on publish.
release.release_title = Release title release.title = Title
release.content_with_md = Content with <a href="%s">Markdown</a> release.content = Content
release.write = Write release.write = Write
release.preview = Preview release.preview = Preview
release.content_placeholder = Write some content
release.loading = Loading... release.loading = Loading...
release.prerelease_desc = This is a pre-release release.prerelease_desc = This is a pre-release
release.prerelease_helper = We’ll point out that this release is not production-ready. release.prerelease_helper = We’ll point out that this release is not production-ready.
release.cancel = Cancel
release.publish = Publish Release release.publish = Publish Release
release.save_draft = Save Draft release.save_draft = Save Draft
release.edit_release = Edit Release release.edit_release = Edit Release
release.tag_name_already_exist = Release with this tag name has already existed. release.tag_name_already_exist = Release with this tag name has already existed.
release.downloads = Downloads
[org] [org]
org_name_holder = Organization Name org_name_holder = Organization Name
org_full_name_holder = Organization Full Name
org_name_helper = Great organization names are short and memorable. org_name_helper = Great organization names are short and memorable.
create_org = Create Organization create_org = Create Organization
repo_updated = Updated repo_updated = Updated
@ -805,6 +814,7 @@ users.edit_account = Edit Account
users.is_activated = This account is activated users.is_activated = This account is activated
users.is_admin = This account has administrator permissions users.is_admin = This account has administrator permissions
users.allow_git_hook = This account has permissions to create Git hooks users.allow_git_hook = This account has permissions to create Git hooks
users.allow_import_local = This account has permissions to import local repositories
users.update_profile = Update Account Profile users.update_profile = Update Account Profile
users.delete_account = Delete This Account users.delete_account = Delete This Account
users.still_own_repo = This account still has ownership over at least one repository, you have to delete or transfer them first. users.still_own_repo = This account still has ownership over at least one repository, you have to delete or transfer them first.

48
conf/locale/locale_es-ES.ini

@ -111,7 +111,7 @@ admin_title=Configuración de la Cuenta de Administrador
admin_name=Nombre de usuario admin_name=Nombre de usuario
admin_password=Contraseña admin_password=Contraseña
confirm_password=Confirmar Contraseña confirm_password=Confirmar Contraseña
admin_email=Correo electrónico admin_email=Admin E-mail
install_gogs=Instalar Gogs install_gogs=Instalar Gogs
test_git_failed=Fallo al probar el comando 'git': %v test_git_failed=Fallo al probar el comando 'git': %v
sqlite3_not_available=Tu versión no soporta SQLite3, por favor descarga el binario oficial desde %s, NO la versión de gobuild. sqlite3_not_available=Tu versión no soporta SQLite3, por favor descarga el binario oficial desde %s, NO la versión de gobuild.
@ -148,7 +148,6 @@ forgot_password=He olvidado mi contraseña
forget_password=¿Has olvidado tu contraseña? forget_password=¿Has olvidado tu contraseña?
sign_up_now=¿Necesitas una cuenta? Regístrate ahora. sign_up_now=¿Necesitas una cuenta? Regístrate ahora.
confirmation_mail_sent_prompt=Un nuevo correo de confirmación se ha enviado a <b>%s</b>. Por favor, comprueba tu bandeja de entrada en las siguientes %d horas para completar el proceso de registro. confirmation_mail_sent_prompt=Un nuevo correo de confirmación se ha enviado a <b>%s</b>. Por favor, comprueba tu bandeja de entrada en las siguientes %d horas para completar el proceso de registro.
sign_in_to_account=Inicie sesión en su cuenta
active_your_account=Activa tu cuenta active_your_account=Activa tu cuenta
resent_limit_prompt=Lo sentimos, estás solicitando el reenvío del mail de activación con demasiada frecuencia. Por favor, espera 3 minutos. resent_limit_prompt=Lo sentimos, estás solicitando el reenvío del mail de activación con demasiada frecuencia. Por favor, espera 3 minutos.
has_unconfirmed_mail=Hola %s, tu correo electrónico (<b>%s</b>) no está confirmado. Si no has recibido un correo de confirmación o necesitas que lo enviemos de nuevo, por favor, haz click en el siguiente botón. has_unconfirmed_mail=Hola %s, tu correo electrónico (<b>%s</b>) no está confirmado. Si no has recibido un correo de confirmación o necesitas que lo enviemos de nuevo, por favor, haz click en el siguiente botón.
@ -192,6 +191,7 @@ min_size_error=` debe contener al menos %s caracteres.`
max_size_error=` debe contener como máximo %s caracteres.` max_size_error=` debe contener como máximo %s caracteres.`
email_error=` no es una dirección de correo válida.` email_error=` no es una dirección de correo válida.`
url_error=` no es una URL válida.` url_error=` no es una URL válida.`
include_error=` must contain substring '%s'.`
unknown_error=Error desconocido: unknown_error=Error desconocido:
captcha_incorrect=El captcha no es válido. captcha_incorrect=El captcha no es válido.
password_not_match=La contraseña de confirmación no coincide. password_not_match=La contraseña de confirmación no coincide.
@ -334,6 +334,7 @@ repo_name=Nombre del Repositorio
repo_name_helper=Los grandes nombres de repositorios son cortos, memorables y <strong>únicos</strong>. repo_name_helper=Los grandes nombres de repositorios son cortos, memorables y <strong>únicos</strong>.
visibility=Visibilidad visibility=Visibilidad
visiblity_helper=Este repositorio es <span class="ui red text">Privado</span> visiblity_helper=Este repositorio es <span class="ui red text">Privado</span>
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
visiblity_fork_helper=(Este cambio afectará a todos los forks) visiblity_fork_helper=(Este cambio afectará a todos los forks)
fork_repo=Hacer Fork del repositorio fork_repo=Hacer Fork del repositorio
fork_from=Crear un Fork desde fork_from=Crear un Fork desde
@ -359,13 +360,14 @@ migrate_type_helper=Este repositorio será un <span class="text blue">mirror</sp
migrate_repo=Migrar Repositorio migrate_repo=Migrar Repositorio
migrate.clone_address=Clonar Dirección migrate.clone_address=Clonar Dirección
migrate.clone_address_desc=Puede ser una URL HTTP/HTTPS/GIT o una ruta local del servidor. migrate.clone_address_desc=Puede ser una URL HTTP/HTTPS/GIT o una ruta local del servidor.
migrate.permission_denied=You are not allowed to import local repositories.
migrate.invalid_local_path=Rutal local inválida, no existe o no es un directorio. migrate.invalid_local_path=Rutal local inválida, no existe o no es un directorio.
forked_from=forked de forked_from=forked de
fork_from_self=Eres el propietario del repositorio, ¡no puedes hacer fork! fork_from_self=Eres el propietario del repositorio, ¡no puedes hacer fork!
copy_link=Copiar copy_link=Copiar
copy_link_success=Copied! copy_link_success=Copiado!
copy_link_error=Press ⌘-C or Ctrl-C to copy copy_link_error=Presione ⌘ + C o Ctrl-C para copiar
click_to_copy=Copiar al portapapeles click_to_copy=Copiar al portapapeles
copied=Copiado correctamente copied=Copiado correctamente
clone_helper=¿Necesitas ayuda con el clone? ¡Consulta la <a target="_blank" href="%s">Ayuda</a>! clone_helper=¿Necesitas ayuda con el clone? ¡Consulta la <a target="_blank" href="%s">Ayuda</a>!
@ -454,9 +456,9 @@ issues.num_comments=%d comentarios
issues.commented_at=`comentada <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at=`comentada <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=Aun no existe contenido. issues.no_content=Aun no existe contenido.
issues.close_issue=Cerrar issues.close_issue=Cerrar
issues.close_comment_issue=Cerrar y Comentar issues.close_comment_issue=Comentar y cerrar
issues.reopen_issue=Reabrir issues.reopen_issue=Reabrir
issues.reopen_comment_issue=Reabrir y Comentar issues.reopen_comment_issue=Comentar y reabrir
issues.create_comment=Comentar issues.create_comment=Comentar
issues.closed_at=`cerrada <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`cerrada <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`reabierta <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`reabierta <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -498,10 +500,12 @@ pulls.reopen_to_merge=Por favor reabra este pull request para proceder con la op
pulls.merged=Fuisionado pulls.merged=Fuisionado
pulls.has_merged=¡Este pull request se ha completado con éxito! pulls.has_merged=¡Este pull request se ha completado con éxito!
pulls.data_broken=Los datos de este pull request ya no están disponibles porque se ha eliminado la información del fork. pulls.data_broken=Los datos de este pull request ya no están disponibles porque se ha eliminado la información del fork.
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
pulls.can_auto_merge_desc=Puede realizar la operación auto-fusionado en este pull request. pulls.can_auto_merge_desc=Puede realizar la operación auto-fusionado en este pull request.
pulls.cannot_auto_merge_desc=No puede realizar la operación de auto-fusionado porque existen conflictos entre los commits. pulls.cannot_auto_merge_desc=No puede realizar la operación de auto-fusionado porque existen conflictos entre los commits.
pulls.cannot_auto_merge_helper=Por favor use la línea de comandos para resolverlo. pulls.cannot_auto_merge_helper=Por favor use la línea de comandos para resolverlo.
pulls.merge_pull_request=Fusionar Pull Request pulls.merge_pull_request=Fusionar Pull Request
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
milestones.new=Nuevo Milestone milestones.new=Nuevo Milestone
milestones.open_tab=%d abiertas milestones.open_tab=%d abiertas
@ -516,7 +520,7 @@ milestones.title=Título
milestones.desc=Descripción milestones.desc=Descripción
milestones.due_date=Fecha límite (opcional) milestones.due_date=Fecha límite (opcional)
milestones.clear=Eliminar milestones.clear=Eliminar
milestones.invalid_due_date_format=El formato de la fecha límite no es válido, debe ser 'y-mm-dd'. milestones.invalid_due_date_format=El formato de la fecha límite no es válido, debe ser 'yyyy-mm-dd'.
milestones.create_success=¡El milestone '%s' ha sido creado con éxito! milestones.create_success=¡El milestone '%s' ha sido creado con éxito!
milestones.edit=Editar Milestone milestones.edit=Editar Milestone
milestones.edit_subheader=Use una buena descripción en el milestone para no confundir al resto de usuarios. milestones.edit_subheader=Use una buena descripción en el milestone para no confundir al resto de usuarios.
@ -648,6 +652,7 @@ release.tag_name_already_exist=Ya existe una Release con esta etiqueta.
[org] [org]
org_name_holder=Nombre de la Organización org_name_holder=Nombre de la Organización
org_full_name_holder=Organization Full Name
org_name_helper=Los grandes nombres de organizaciones son cortos y memorables. org_name_helper=Los grandes nombres de organizaciones son cortos y memorables.
create_org=Crear Organización create_org=Crear Organización
repo_updated=Actualizado repo_updated=Actualizado
@ -804,6 +809,7 @@ users.edit_account=Editar Cuenta
users.is_activated=Esta cuenta está activada users.is_activated=Esta cuenta está activada
users.is_admin=Esta cuenta tiene permisos de administrador users.is_admin=Esta cuenta tiene permisos de administrador
users.allow_git_hook=Esta cuenta tiene permisos para crear hooks de Git users.allow_git_hook=Esta cuenta tiene permisos para crear hooks de Git
users.allow_import_local=This account has permissions to import local repositories
users.update_profile=Actualizar Perfil de la Cuenta users.update_profile=Actualizar Perfil de la Cuenta
users.delete_account=Eliminar esta Cuenta users.delete_account=Eliminar esta Cuenta
users.still_own_repo=Esta cuenta es propietaria de uno o más repositorios, tienes que borrarlos o transferirlos primero. users.still_own_repo=Esta cuenta es propietaria de uno o más repositorios, tienes que borrarlos o transferirlos primero.
@ -964,20 +970,20 @@ compare_2_commits=Ver la comparación de estos 2 commits
ago=hace ago=hace
from_now=desde ahora from_now=desde ahora
now=ahora now=ahora
1s=1 segundo %s 1s=%s 1 segundo
1m=1 minuto %s 1m=%s 1 minuto
1h=1 hora %s 1h=%s 1 hora
1d=1 día %s 1d=%s 1 día
1w=1 semana %s 1w=%s 1 semana
1mon=1 mes %s 1mon=%s 1 mes
1y=1 año %s 1y=%s 1 año
seconds=%d segundos %s seconds=%s %d segundos
minutes=%d minutos %s minutes=%s %d minutos
hours=%d horas %s hours=%s %d horas
days=%d días %s days=%s %d días
weeks=%d semanas %s weeks=%s %d semanas
months=%d meses %s months=%s %d meses
years=%d años %s years=%s %d años
raw_seconds=segundos raw_seconds=segundos
raw_minutes=minutos raw_minutes=minutos

24
conf/locale/locale_fr-FR.ini

@ -7,7 +7,7 @@ help=Aide
sign_in=Connexion sign_in=Connexion
sign_out=Déconnexion sign_out=Déconnexion
sign_up=Créer un compte sign_up=Créer un compte
register=S'inscrire register=Inscription
website=Site Web website=Site Web
version=Version version=Version
page=Page page=Page
@ -111,7 +111,7 @@ admin_title=Paramètres du Compte Administrateur
admin_name=Nom d'Utilisateur admin_name=Nom d'Utilisateur
admin_password=Mot de Passe admin_password=Mot de Passe
confirm_password=Confirmez le Mot de Passe confirm_password=Confirmez le Mot de Passe
admin_email=E-mail admin_email=E-mail de l'administrateur
install_gogs=Installer Gogs install_gogs=Installer Gogs
test_git_failed=Tentative de commande "git" échouée : %v test_git_failed=Tentative de commande "git" échouée : %v
sqlite3_not_available=Votre version publiée ne prend pas en charge SQLite3. Veuillez télécharger la version binaire officielle à cette adresse %s. sqlite3_not_available=Votre version publiée ne prend pas en charge SQLite3. Veuillez télécharger la version binaire officielle à cette adresse %s.
@ -148,7 +148,6 @@ forgot_password=Mot de Passe oublié
forget_password=Mot de Passe oublié ? forget_password=Mot de Passe oublié ?
sign_up_now=Pas de compte ? Créer maintenant. sign_up_now=Pas de compte ? Créer maintenant.
confirmation_mail_sent_prompt=Un nouveau mail de confirmation à été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans un délai de %d heures pour compléter votre enregistrement. confirmation_mail_sent_prompt=Un nouveau mail de confirmation à été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans un délai de %d heures pour compléter votre enregistrement.
sign_in_to_account=Connectez-vous à votre compte
active_your_account=Activer votre Compte active_your_account=Activer votre Compte
resent_limit_prompt=Désolé, vos tentatives d'activation sont trop fréquentes. Veuillez réessayer dans 3 minutes. resent_limit_prompt=Désolé, vos tentatives d'activation sont trop fréquentes. Veuillez réessayer dans 3 minutes.
has_unconfirmed_mail=Bonjour %s, votre adresse courriel (<b>%s</b>) n'a pas été confirmée. Si vous n'avez reçu aucun courriel de confirmation ou souhaitez renouveler l'envoi, appuyez sur le bouton ci-dessous. has_unconfirmed_mail=Bonjour %s, votre adresse courriel (<b>%s</b>) n'a pas été confirmée. Si vous n'avez reçu aucun courriel de confirmation ou souhaitez renouveler l'envoi, appuyez sur le bouton ci-dessous.
@ -192,6 +191,7 @@ min_size_error=` %s caractères minimum `
max_size_error=` %s caractères maximum ` max_size_error=` %s caractères maximum `
email_error=` adresse e-mail invalide ` email_error=` adresse e-mail invalide `
url_error=` URL invalide ` url_error=` URL invalide `
include_error=`doit contenir la sous-chaîne '%s'.`
unknown_error=Erreur inconnue : unknown_error=Erreur inconnue :
captcha_incorrect=Le Captcha ne correspond pas. captcha_incorrect=Le Captcha ne correspond pas.
password_not_match=Le mot de passe et la confirmation de mot de passe ne correspondent pas. password_not_match=Le mot de passe et la confirmation de mot de passe ne correspondent pas.
@ -334,6 +334,7 @@ repo_name=Nom du Référentiel
repo_name_helper=Idéalement, le nom d'un dépot devrait être court, mémorable et <strong>unique</strong>. repo_name_helper=Idéalement, le nom d'un dépot devrait être court, mémorable et <strong>unique</strong>.
visibility=Visibilité visibility=Visibilité
visiblity_helper=Ce dépôt est <span class="ui red text"> privé</span> visiblity_helper=Ce dépôt est <span class="ui red text"> privé</span>
visiblity_helper_forced=L'administrateur du site a forcé tous les nouveaux dépôts à être <span class="ui red text">privés</span>
visiblity_fork_helper=(Les changement de cette valeur affectera tous les forks) visiblity_fork_helper=(Les changement de cette valeur affectera tous les forks)
fork_repo=Scinder le dépôt fork_repo=Scinder le dépôt
fork_from=Embranchement de fork_from=Embranchement de
@ -359,13 +360,14 @@ migrate_type_helper=Ce dépôt sera un <span class="text blue"> miroir</span>
migrate_repo=Migrer le dépôt migrate_repo=Migrer le dépôt
migrate.clone_address=Adresse du clone migrate.clone_address=Adresse du clone
migrate.clone_address_desc=Cela peut être une URL HTTP/HTTPS/GIT ou un chemin d'accès local. migrate.clone_address_desc=Cela peut être une URL HTTP/HTTPS/GIT ou un chemin d'accès local.
migrate.permission_denied=Vous n'êtes pas autorisé à importer des dépôts locaux.
migrate.invalid_local_path=Chemin local non valide, non existant ou n'étant pas un dossier. migrate.invalid_local_path=Chemin local non valide, non existant ou n'étant pas un dossier.
forked_from=dérivé depuis forked_from=dérivé depuis
fork_from_self=Vous nous ne pouvez pas scinder un dépôt que vous possédez déja ! fork_from_self=Vous nous ne pouvez pas scinder un dépôt que vous possédez déja !
copy_link=Copier copy_link=Copier
copy_link_success=Copied! copy_link_success=Copié!
copy_link_error=Press ⌘-C or Ctrl-C to copy copy_link_error=Appuyez sur ⌘-C ou Ctrl-C pour copier
click_to_copy=Copier dans le presse-papiers click_to_copy=Copier dans le presse-papiers
copied=Copié copied=Copié
clone_helper=Besoin d'aide pour le clonage ? Visitez <a target="_blank" href="%s"> l'aider</a> ! clone_helper=Besoin d'aide pour le clonage ? Visitez <a target="_blank" href="%s"> l'aider</a> !
@ -380,7 +382,7 @@ quick_guide=Introduction rapide
clone_this_repo=Cloner ce dépôt clone_this_repo=Cloner ce dépôt
create_new_repo_command=Créer un nouveau dépôt en ligne de commande create_new_repo_command=Créer un nouveau dépôt en ligne de commande
push_exist_repo=Soumettre un dépôt existant par ligne de commande push_exist_repo=Soumettre un dépôt existant par ligne de commande
repo_is_empty=This repository is empty, please come back later! repo_is_empty=Ce référentiel est vide, veuillez revenir plus tard !
branch=Branche branch=Branche
@ -454,9 +456,9 @@ issues.num_comments=%d commentaires
issues.commented_at='commenté à <a id="%[1]s" href="#%[1]s"> %[2]s'</a> issues.commented_at='commenté à <a id="%[1]s" href="#%[1]s"> %[2]s'</a>
issues.no_content=Il n'existe pas encore de contenu. issues.no_content=Il n'existe pas encore de contenu.
issues.close_issue=Fermer issues.close_issue=Fermer
issues.close_comment_issue=Fermer et commenter issues.close_comment_issue=Commenter et fermer
issues.reopen_issue=Réouvrir issues.reopen_issue=Réouvrir
issues.reopen_comment_issue=Réouvrir et commenter issues.reopen_comment_issue=Commenter et réouvrir
issues.create_comment=Créer un commentaire issues.create_comment=Créer un commentaire
issues.closed_at="fermé à <a id="%[1]s"href="#%[1]s"> %[2]s"</a> issues.closed_at="fermé à <a id="%[1]s"href="#%[1]s"> %[2]s"</a>
issues.reopened_at='réouvert à <a id="%[1]s" href="#%[1]s"> %[2]s'</a> issues.reopened_at='réouvert à <a id="%[1]s" href="#%[1]s"> %[2]s'</a>
@ -498,10 +500,12 @@ pulls.reopen_to_merge=Veuillez rouvrir cette demande de Pull Request pour effect
pulls.merged=Fusionné pulls.merged=Fusionné
pulls.has_merged=Cette Pull Request a été fusionnée avec succès ! pulls.has_merged=Cette Pull Request a été fusionnée avec succès !
pulls.data_broken=Les données de cette Pull Request a été rompues en raison de la suppression d'informations du Fork. pulls.data_broken=Les données de cette Pull Request a été rompues en raison de la suppression d'informations du Fork.
pulls.is_checking=La recherche de conflicts est toujours en cours, veuillez rafraichir la page dans quelques instants.
pulls.can_auto_merge_desc=Vous pouvez effectuer d'opération de fusion automatique sur cette demande de Pull Request. pulls.can_auto_merge_desc=Vous pouvez effectuer d'opération de fusion automatique sur cette demande de Pull Request.
pulls.cannot_auto_merge_desc=Vous ne pouvez effectuer des opération de fusion automatique car il y a des conflits entre les Commits. pulls.cannot_auto_merge_desc=Vous ne pouvez effectuer des opération de fusion automatique car il y a des conflits entre les Commits.
pulls.cannot_auto_merge_helper=Veuillez utiliser l'outil en ligne de commande pour le résoudre. pulls.cannot_auto_merge_helper=Veuillez utiliser l'outil en ligne de commande pour le résoudre.
pulls.merge_pull_request=Fusionner la Pull Request pulls.merge_pull_request=Fusionner la Pull Request
pulls.open_unmerged_pull_exists=`Vous ne pouvez effectuer une réouverture car il y a déjà une pull-request ouverte (#%d) depuis le même dépôt avec les mêmes informations de fusion et est en attente de fusion.`
milestones.new=Nouveau Jalon milestones.new=Nouveau Jalon
milestones.open_tab=%d Ouvert milestones.open_tab=%d Ouvert
@ -516,7 +520,7 @@ milestones.title=Titre
milestones.desc=Description milestones.desc=Description
milestones.due_date=Date d'échéance (facultatif) milestones.due_date=Date d'échéance (facultatif)
milestones.clear=Effacer milestones.clear=Effacer
milestones.invalid_due_date_format=Le format de la date d'échéance est invalide, il doit être comme suit 'année-mm-jj'. milestones.invalid_due_date_format=Le format de la date d'échéance est invalide, il doit être comme suit 'AAAA-mm-jj'.
milestones.create_success=Le Jalon '%s' a été crée avec succès ! milestones.create_success=Le Jalon '%s' a été crée avec succès !
milestones.edit=Éditer le Jalon milestones.edit=Éditer le Jalon
milestones.edit_subheader=Utilisez une description claire pour les jalons pour ne pas induire les gens en erreur. milestones.edit_subheader=Utilisez une description claire pour les jalons pour ne pas induire les gens en erreur.
@ -648,6 +652,7 @@ release.tag_name_already_exist=Une publication avec ce nom de tag a déjà exist
[org] [org]
org_name_holder=Nom d'organisation org_name_holder=Nom d'organisation
org_full_name_holder=Nom complet de l'organisation
org_name_helper=Idéalement, un nom d'organisation devrait être court et mémorable. org_name_helper=Idéalement, un nom d'organisation devrait être court et mémorable.
create_org=Créer une organisation create_org=Créer une organisation
repo_updated=Mis à jour repo_updated=Mis à jour
@ -804,6 +809,7 @@ users.edit_account=Modifier le Compte
users.is_activated=Ce compte est activé users.is_activated=Ce compte est activé
users.is_admin=Ce compte possède un niveau d'accès administrateur users.is_admin=Ce compte possède un niveau d'accès administrateur
users.allow_git_hook=Ce compte dispose des autorisations pour créer des crochets de Git users.allow_git_hook=Ce compte dispose des autorisations pour créer des crochets de Git
users.allow_import_local=Ce compte dispose des permissions nécessaire à l'import des dépôts locaux
users.update_profile=Mettre le profil à jour users.update_profile=Mettre le profil à jour
users.delete_account=Supprimer ce Compte users.delete_account=Supprimer ce Compte
users.still_own_repo=Ce compte possède toujours des dépôts. Vous devez d'abord les supprimer ou les transférer. users.still_own_repo=Ce compte possède toujours des dépôts. Vous devez d'abord les supprimer ou les transférer.

36
conf/locale/locale_it-IT.ini

@ -67,8 +67,8 @@ path=Percorso
sqlite_helper=The file path of SQLite3 or TiDB database. sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty. err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-". err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account. no_admin_and_disable_registration=Non puoi disabilitare la registrazione senza aver creato un amministratore.
err_empty_admin_password=Admin password cannot be empty. err_empty_admin_password=La password dell'amministratore non puo' essere vuota.
general_title=Impostazioni di Base dell'Applicazione general_title=Impostazioni di Base dell'Applicazione
app_name=Nome Applicazione app_name=Nome Applicazione
@ -111,7 +111,7 @@ admin_title=Impostazioni Account Amministratore
admin_name=Nome utente admin_name=Nome utente
admin_password=Password admin_password=Password
confirm_password=Conferma Password confirm_password=Conferma Password
admin_email=E-mail admin_email=Admin E-mail
install_gogs=Installare Gogs install_gogs=Installare Gogs
test_git_failed=Fallito il test del comando git: %v test_git_failed=Fallito il test del comando git: %v
sqlite3_not_available=Questa versione non supporta SQLite3, si prega di scaricare la versione binaria ufficiale da %s, NON la versione gobuild. sqlite3_not_available=Questa versione non supporta SQLite3, si prega di scaricare la versione binaria ufficiale da %s, NON la versione gobuild.
@ -130,7 +130,7 @@ my_repos=I miei Repository
collaborative_repos=Repository Condivisi collaborative_repos=Repository Condivisi
my_orgs=Le mie Organizzazioni my_orgs=Le mie Organizzazioni
my_mirrors=I miei Mirror my_mirrors=I miei Mirror
view_home=View %s view_home=Vedi %s
issues.in_your_repos=Nei tuoi repository issues.in_your_repos=Nei tuoi repository
@ -148,7 +148,6 @@ forgot_password=Password dimenticata
forget_password=Password dimenticata? forget_password=Password dimenticata?
sign_up_now=Bisogno di un account? Iscriviti ora. sign_up_now=Bisogno di un account? Iscriviti ora.
confirmation_mail_sent_prompt=Una nuova email di conferma è stata inviata a <b>%s</b>, verifica la tua casella di posta entro le prossime %d ore per completare la registrazione. confirmation_mail_sent_prompt=Una nuova email di conferma è stata inviata a <b>%s</b>, verifica la tua casella di posta entro le prossime %d ore per completare la registrazione.
sign_in_to_account=Accedi al tuo account
active_your_account=Attiva il tuo Account active_your_account=Attiva il tuo Account
resent_limit_prompt=Siamo spiacenti, si stanno inviando e-mail di attivazione troppo spesso. Si prega di attendere 3 minuti. resent_limit_prompt=Siamo spiacenti, si stanno inviando e-mail di attivazione troppo spesso. Si prega di attendere 3 minuti.
has_unconfirmed_mail=Ciao %s, hai un indirizzo di posta elettronica non confermato (<b>%s</b>). Se non hai ricevuto una e-mail di conferma o vuoi riceverla nuovamente, fare clic sul pulsante qui sotto. has_unconfirmed_mail=Ciao %s, hai un indirizzo di posta elettronica non confermato (<b>%s</b>). Se non hai ricevuto una e-mail di conferma o vuoi riceverla nuovamente, fare clic sul pulsante qui sotto.
@ -161,7 +160,7 @@ reset_password_helper=Clicca qui per reimpostare la password
password_too_short=La lunghezza della password non può essere meno 6 caratteri. password_too_short=La lunghezza della password non può essere meno 6 caratteri.
[mail] [mail]
activate_account=Please activate your account activate_account=Per favore attiva il tuo account
activate_email=Verifica il tuo indirizzo e-mail activate_email=Verifica il tuo indirizzo e-mail
reset_password=Reimposta la tua password reset_password=Reimposta la tua password
register_success=Registrazione completata con successo, Benvenuto register_success=Registrazione completata con successo, Benvenuto
@ -192,6 +191,7 @@ min_size_error=` deve contenere almeno %s caratteri.`
max_size_error=` deve contenere massimo %s caratteri.` max_size_error=` deve contenere massimo %s caratteri.`
email_error=` non è un indirizzo e-mail valido.` email_error=` non è un indirizzo e-mail valido.`
url_error=` non è un URL valido.` url_error=` non è un URL valido.`
include_error=` deve contenere la stringa '%s'.`
unknown_error=Errore sconosciuto: unknown_error=Errore sconosciuto:
captcha_incorrect=Il Captcha non corrisponde. captcha_incorrect=Il Captcha non corrisponde.
password_not_match=Le due password non corrispondono. password_not_match=Le due password non corrispondono.
@ -298,12 +298,12 @@ add_key_success=New SSH key '%s' has been added successfully!
delete_key=Elimina delete_key=Elimina
ssh_key_deletion=SSH Key Deletion ssh_key_deletion=SSH Key Deletion
ssh_key_deletion_desc=Delete this SSH key will remove all related accesses for your account. Do you want to continue? ssh_key_deletion_desc=Delete this SSH key will remove all related accesses for your account. Do you want to continue?
ssh_key_deletion_success=SSH key has been deleted successfully! ssh_key_deletion_success=La chiave SSH e' stata cancellata con successo!
add_on=Aggiunto il add_on=Aggiunto il
last_used=Ultimo accesso il last_used=Ultimo accesso il
no_activity=Nessuna attività recente no_activity=Nessuna attività recente
key_state_desc=Hai utilizzato questa chiave negli ultimi 7 giorni key_state_desc=Hai utilizzato questa chiave negli ultimi 7 giorni
token_state_desc=This token is used in last 7 days token_state_desc=Questo token e' satato utilizzato negli ultimi 7 giorni
manage_social=Gestisci gli Account Sociali Associati manage_social=Gestisci gli Account Sociali Associati
social_desc=Questa è un elenco degli account sociali associati. Rimuovere qualsiasi account che non si riconosce. social_desc=Questa è un elenco degli account sociali associati. Rimuovere qualsiasi account che non si riconosce.
@ -334,6 +334,7 @@ repo_name=Nome Repository
repo_name_helper=I migliori nomi dei repository sono brevi, facili da memorizzare e <strong>univoci</strong>. repo_name_helper=I migliori nomi dei repository sono brevi, facili da memorizzare e <strong>univoci</strong>.
visibility=Visibilità visibility=Visibilità
visiblity_helper=This repository is <span class="ui red text">Private</span> visiblity_helper=This repository is <span class="ui red text">Private</span>
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
visiblity_fork_helper=(Change of this value will affect all forks) visiblity_fork_helper=(Change of this value will affect all forks)
fork_repo=Forka Repository fork_repo=Forka Repository
fork_from=Forka da fork_from=Forka da
@ -359,12 +360,13 @@ migrate_type_helper=This repository will be a <span class="text blue">mirror</sp
migrate_repo=Migra Repository migrate_repo=Migra Repository
migrate.clone_address=Duplica Indirizzo migrate.clone_address=Duplica Indirizzo
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path. migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path.
migrate.permission_denied=You are not allowed to import local repositories.
migrate.invalid_local_path=Percorso locale non valido, non esiste o non è una cartella. migrate.invalid_local_path=Percorso locale non valido, non esiste o non è una cartella.
forked_from=forkato da forked_from=forkato da
fork_from_self=Non puoi forkare il tuo stesso repository! fork_from_self=Non puoi forkare il tuo stesso repository!
copy_link=Copia copy_link=Copia
copy_link_success=Copied! copy_link_success=Copiato!
copy_link_error=Press ⌘-C or Ctrl-C to copy copy_link_error=Press ⌘-C or Ctrl-C to copy
click_to_copy=Copia negli appunti click_to_copy=Copia negli appunti
copied=OK copiato copied=OK copiato
@ -437,7 +439,7 @@ issues.filter_type.all_issues=Tutti i problemi
issues.filter_type.assigned_to_you=Assegnati a te issues.filter_type.assigned_to_you=Assegnati a te
issues.filter_type.created_by_you=Creati da te issues.filter_type.created_by_you=Creati da te
issues.filter_type.mentioning_you=Che ti riguardano issues.filter_type.mentioning_you=Che ti riguardano
issues.filter_sort=Sort issues.filter_sort=Ordina
issues.filter_sort.latest=Newest issues.filter_sort.latest=Newest
issues.filter_sort.oldest=Oldest issues.filter_sort.oldest=Oldest
issues.filter_sort.recentupdate=Recently updated issues.filter_sort.recentupdate=Recently updated
@ -453,10 +455,10 @@ issues.closed_title=Closed
issues.num_comments=%d comments issues.num_comments=%d comments
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=There is no content yet. issues.no_content=There is no content yet.
issues.close_issue=Close issues.close_issue=Chiudi
issues.close_comment_issue=Close and comment issues.close_comment_issue=Comment and close
issues.reopen_issue=Reopen issues.reopen_issue=Reopen
issues.reopen_comment_issue=Reopen and comment issues.reopen_comment_issue=Comment and reopen
issues.create_comment=Commento issues.create_comment=Commento
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -464,7 +466,7 @@ issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%
issues.poster=Poster issues.poster=Poster
issues.admin=Amministratore issues.admin=Amministratore
issues.owner=Proprietario issues.owner=Proprietario
issues.sign_up_for_free=Sign up for free issues.sign_up_for_free=Registrati gratuitamente
issues.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a> issues.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a>
issues.edit=Edit issues.edit=Edit
issues.cancel=Cancel issues.cancel=Cancel
@ -498,10 +500,12 @@ pulls.reopen_to_merge=Please reopen this pull request to perform merge operation
pulls.merged=Merged pulls.merged=Merged
pulls.has_merged=This pull request has been merged successfully! pulls.has_merged=This pull request has been merged successfully!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information. pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request. pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits. pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it. pulls.cannot_auto_merge_helper=Please use command line tool to solve it.
pulls.merge_pull_request=Unisci Pull Request pulls.merge_pull_request=Unisci Pull Request
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
milestones.new=New Milestone milestones.new=New Milestone
milestones.open_tab=%d Open milestones.open_tab=%d Open
@ -516,7 +520,7 @@ milestones.title=Title
milestones.desc=Description milestones.desc=Description
milestones.due_date=Due Date (optional) milestones.due_date=Due Date (optional)
milestones.clear=Clear milestones.clear=Clear
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'. milestones.invalid_due_date_format=Due date format is invalid, must be 'yyyy-mm-dd'.
milestones.create_success=Milestone '%s' has been created successfully! milestones.create_success=Milestone '%s' has been created successfully!
milestones.edit=Edit Milestone milestones.edit=Edit Milestone
milestones.edit_subheader=Use better description for milestones so people won't be confused. milestones.edit_subheader=Use better description for milestones so people won't be confused.
@ -648,6 +652,7 @@ release.tag_name_already_exist=Un rilascio con questo tag esiste già.
[org] [org]
org_name_holder=Nome dell'Organizzazione org_name_holder=Nome dell'Organizzazione
org_full_name_holder=Organization Full Name
org_name_helper=Le migliori organizzazioni hanno nomi brevi e memorabili. org_name_helper=Le migliori organizzazioni hanno nomi brevi e memorabili.
create_org=Crea Organizzazione create_org=Crea Organizzazione
repo_updated=Aggiornato repo_updated=Aggiornato
@ -804,6 +809,7 @@ users.edit_account=Modifica Account
users.is_activated=Questo account è attivato users.is_activated=Questo account è attivato
users.is_admin=Questo account ha permessi di amministratore users.is_admin=Questo account ha permessi di amministratore
users.allow_git_hook=Questo account ha il permesso di creare hooks di Git users.allow_git_hook=Questo account ha il permesso di creare hooks di Git
users.allow_import_local=This account has permissions to import local repositories
users.update_profile=Aggiornare Profilo Account users.update_profile=Aggiornare Profilo Account
users.delete_account=Elimina Questo Account users.delete_account=Elimina Questo Account
users.still_own_repo=Questo account possiede ancora almeno un repository, devi prima cancellarli o trasferirli. users.still_own_repo=Questo account possiede ancora almeno un repository, devi prima cancellarli o trasferirli.

282
conf/locale/locale_ja-JP.ini

@ -53,8 +53,8 @@ code=コード
[install] [install]
install=インストール install=インストール
title=初回実行のインストール手順 title=初回実行のインストール手順
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page! docker_helper=DockerでGogsを稼動する場合、このページに変更を加えるまえに、 <a target="_blank" href="%s">Guidelines</a>をよく読んでください!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB. requite_db_desc=Gogs は、MySQL、PostgreSQL、SQLite3 または TiDB が必要です。
db_title=データベース設定 db_title=データベース設定
db_type=データベースの種類 db_type=データベースの種類
host=ホスト host=ホスト
@ -64,11 +64,11 @@ db_name=データベース名
db_helper=Mysql INNODB エンジン utf8_general_ci の文字セットを使用してください。 db_helper=Mysql INNODB エンジン utf8_general_ci の文字セットを使用してください。
ssl_mode=SSL モード ssl_mode=SSL モード
path=パス path=パス
sqlite_helper=The file path of SQLite3 or TiDB database. sqlite_helper=SQLite3 または TiDB のデータベースのファイル パス。
err_empty_db_path=SQLite3 or TiDB database path cannot be empty. err_empty_db_path=SQLite3 または TiDB データベースのパスを空にすることはできません。
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-". err_invalid_tidb_name=TiDB データベース名は文字"."と"-"を許可しない。
no_admin_and_disable_registration=You cannot disable registration without creating an admin account. no_admin_and_disable_registration=You cannot disable registration without creating an admin account.
err_empty_admin_password=Admin password cannot be empty. err_empty_admin_password=管理者パスワードは空白にできません。
general_title=Gogs の全般設定 general_title=Gogs の全般設定
app_name=アプリケーション名 app_name=アプリケーション名
@ -80,7 +80,7 @@ run_user_helper=ユーザーはリポジトリ ルートパスへのアクセス
domain=ドメイン domain=ドメイン
domain_helper=これはSSHクローンURLに影響する。 domain_helper=これはSSHクローンURLに影響する。
ssh_port=SSH ポート ssh_port=SSH ポート
ssh_port_helper=Port number which your SSH server is using, leave it empty to disable SSH feature. ssh_port_helper=あならのSSHサーバおポート番号、SSH機能を無効するにはここを空白のままにしてください。
http_port=HTTP ポート http_port=HTTP ポート
http_port_helper=アプリケーションが待ち受けするポート番号。 http_port_helper=アプリケーションが待ち受けするポート番号。
app_url=アプリケーションの URL app_url=アプリケーションの URL
@ -103,7 +103,7 @@ disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uplo
disable_registration=自己登録を無効にする disable_registration=自己登録を無効にする
disable_registration_popup=自己登録を無効にし、管理者のみがアカウント作成できる disable_registration_popup=自己登録を無効にし、管理者のみがアカウント作成できる
enable_captcha=Captchaを有効にする enable_captcha=Captchaを有効にする
enable_captcha_popup=Require validate captcha for user self-registration. enable_captcha_popup=ユーザによる自己登録のため、有効なcaptchaが必要です。
require_sign_in_view=サインインしたユーザのみページ閲覧を許可 require_sign_in_view=サインインしたユーザのみページ閲覧を許可
require_sign_in_view_popup=サインインしたユーザのみがページを閲覧できます。ビジターはサインインもしくはサインアップページのみ見られます。 require_sign_in_view_popup=サインインしたユーザのみがページを閲覧できます。ビジターはサインインもしくはサインアップページのみ見られます。
admin_setting_desc=今管理者アカウントを作成する必要はありません。ID = 1のユーザ は自動的に管理者の権限を獲得します。 admin_setting_desc=今管理者アカウントを作成する必要はありません。ID = 1のユーザ は自動的に管理者の権限を獲得します。
@ -111,7 +111,7 @@ admin_title=管理者アカウントの設定
admin_name=ユーザ名 admin_name=ユーザ名
admin_password=パスワード admin_password=パスワード
confirm_password=パスワード確認 confirm_password=パスワード確認
admin_email=E-mail admin_email=管理者の電子メール
install_gogs=Gogs をインストール install_gogs=Gogs をインストール
test_git_failed='Git' コマンドテストに失敗: %v test_git_failed='Git' コマンドテストに失敗: %v
sqlite3_not_available=このリリース バージョンは SQLite3 をサポートしていません。gobuild バージョンではない、公式のバイナリ バージョンを %s からダウンロードしてください。 sqlite3_not_available=このリリース バージョンは SQLite3 をサポートしていません。gobuild バージョンではない、公式のバイナリ バージョンを %s からダウンロードしてください。
@ -148,7 +148,6 @@ forgot_password=パスワードを忘れた
forget_password=パスワードを忘れた? forget_password=パスワードを忘れた?
sign_up_now=アカウントが必要ですか?今すぐサインアップ sign_up_now=アカウントが必要ですか?今すぐサインアップ
confirmation_mail_sent_prompt=新しい確認メールを <b>%s</b> に送りました。登録を完了させるために、%d時間以内にあなたのメールボックスを確認してください。 confirmation_mail_sent_prompt=新しい確認メールを <b>%s</b> に送りました。登録を完了させるために、%d時間以内にあなたのメールボックスを確認してください。
sign_in_to_account=Sign in to your account
active_your_account=アカウントをアクティブ active_your_account=アカウントをアクティブ
resent_limit_prompt=申し訳ありませんが、アクティベーションメールは頻繁に送信しています。3 分お待ちください。 resent_limit_prompt=申し訳ありませんが、アクティベーションメールは頻繁に送信しています。3 分お待ちください。
has_unconfirmed_mail=こんにちは %s さん、あなたの電子メール アドレス (<b>%s</b>) は未確認です。もし確認メールをまだ確認できていないか、改めて再送信する場合は、下のボタンをクリックしてください。 has_unconfirmed_mail=こんにちは %s さん、あなたの電子メール アドレス (<b>%s</b>) は未確認です。もし確認メールをまだ確認できていないか、改めて再送信する場合は、下のボタンをクリックしてください。
@ -161,10 +160,10 @@ reset_password_helper=パスワードをリセットするにはここをクリ
password_too_short=6文字未満のパスワードは設定できません。 password_too_short=6文字未満のパスワードは設定できません。
[mail] [mail]
activate_account=Please activate your account activate_account=あなたのアカウントを有効にしてください。
activate_email=Verify your e-mail address activate_email=電子メール アドレスを確認します。
reset_password=Reset your password reset_password=パスワードをリセットします.
register_success=Register success, Welcome register_success=ようこそ、登録成功
[modal] [modal]
yes=はい yes=はい
@ -192,6 +191,7 @@ min_size_error=' 少なくとも %s 文字の必要があります '
max_size_error=' %s 文字以下の必要があります ' max_size_error=' %s 文字以下の必要があります '
email_error=' は有効な電子メール アドレスではない ' email_error=' は有効な電子メール アドレスではない '
url_error=' は有効な URL はありません。 ' url_error=' は有効な URL はありません。 '
include_error=' 文字列 '%s' を含める必要があります。 '
unknown_error=不明なエラー: unknown_error=不明なエラー:
captcha_incorrect=Captcha が一致しませんでした。 captcha_incorrect=Captcha が一致しませんでした。
password_not_match=パスワードと確認用パスワードが一致同していません。 password_not_match=パスワードと確認用パスワードが一致同していません。
@ -252,7 +252,7 @@ location=ロケーション
update_profile=プロファイル更新 update_profile=プロファイル更新
update_profile_success=あなたのプロフィールが更新されました。 update_profile_success=あなたのプロフィールが更新されました。
change_username=ユーザー名が変更されました change_username=ユーザー名が変更されました
change_username_prompt=This change will affect the way how links relate to your account. change_username_prompt=この変更はリンクをアカウントに関連付ける方法に影響します。
continue=続行 continue=続行
cancel=キャンセル cancel=キャンセル
@ -267,7 +267,7 @@ update_avatar_success=あなたのアバターの設定が更新されました
change_password=パスワードを変更 change_password=パスワードを変更
old_password=現在のパスワード old_password=現在のパスワード
new_password=新しいパスワード new_password=新しいパスワード
retype_new_password=Retype New Password retype_new_password=新しいパスワードを再入力します。
password_incorrect=現在のパスワードが正しくありません。 password_incorrect=現在のパスワードが正しくありません。
change_password_success=パスワードが正常に変更されました。今すぐ新しいパスワード経由でサインインすることができます。 change_password_success=パスワードが正常に変更されました。今すぐ新しいパスワード経由でサインインすることができます。
@ -277,9 +277,9 @@ email_desc=あなたのプライマリメールアドレスは、通知やその
primary=プライマリー primary=プライマリー
primary_email=プライマリに設定 primary_email=プライマリに設定
delete_email=削除 delete_email=削除
email_deletion=E-mail Deletion email_deletion=電子メールの削除
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue? email_deletion_desc=この電子メール アドレスを削除すると、あなたのアカウントの関連情報も削除されます。続行しますか。
email_deletion_success=E-mail has been deleted successfully! email_deletion_success=電子メールが正常に削除されました。
add_new_email=新しいe-mailアドレスを追加 add_new_email=新しいe-mailアドレスを追加
add_email=電子メールを追加します。 add_email=電子メールを追加します。
add_email_confirmation_sent='%s' に新しい確認メールを送信しました、次の %d 時間以内に受信トレイを確認し、確認プロセスを完了してください。 add_email_confirmation_sent='%s' に新しい確認メールを送信しました、次の %d 時間以内に受信トレイを確認し、確認プロセスを完了してください。
@ -334,6 +334,7 @@ repo_name=リポジトリ名
repo_name_helper=偉大なリポジトリ名は短い。思い出に残り、そして<strong>一意</strong>だ。 repo_name_helper=偉大なリポジトリ名は短い。思い出に残り、そして<strong>一意</strong>だ。
visibility=ビジビリティ visibility=ビジビリティ
visiblity_helper=このリポジトリは<span class="ui red text">プライベート</span>です。 visiblity_helper=このリポジトリは<span class="ui red text">プライベート</span>です。
visiblity_helper_forced=サイト管理者は、強制的にすべての新しいリポジトリを<span class="ui red text"> プライベート</span> にしています。
visiblity_fork_helper=(この値の変更はすべてのフォークに適用されます) visiblity_fork_helper=(この値の変更はすべてのフォークに適用されます)
fork_repo=フォークのリポジトリ fork_repo=フォークのリポジトリ
fork_from=フォーク元 fork_from=フォーク元
@ -344,8 +345,8 @@ repo_lang_helper=.gitignoreファイルを選択
license=ライセンス license=ライセンス
license_helper=ライセンス ファイルを選択 license_helper=ライセンス ファイルを選択
readme=Readme readme=Readme
readme_helper=Select a readme template readme_helper=Readme ファイルのテンプレートを選択
auto_init=Initialize this repository with selected files and template auto_init=選択されたファイルおよびテンプレートでリポジトリを初期化
create_repo=リポジトリを作成 create_repo=リポジトリを作成
default_branch=デフォルトのブランチ default_branch=デフォルトのブランチ
mirror_interval=ミラー 間隔(時) mirror_interval=ミラー 間隔(時)
@ -355,17 +356,18 @@ form.name_pattern_not_allowed=リポジトリ名のパターン '%s' は許可
need_auth=認証が必要 need_auth=認証が必要
migrate_type=マイグレーションの種類 migrate_type=マイグレーションの種類
migrate_type_helper=This repository will be a <span class="text blue">mirror</span> migrate_type_helper=このリポジトリは、<span class="text blue"> ミラー</span> になります
migrate_repo=リポジトリを移行 migrate_repo=リポジトリを移行
migrate.clone_address=クローンアドレス migrate.clone_address=クローンアドレス
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path. migrate.clone_address_desc=これは、HTTP/HTTPS/GIT URL またはローカル サーバー パスを設定できます。
migrate.permission_denied=You are not allowed to import local repositories.
migrate.invalid_local_path=ローカルパスが無効です。存在しないかディレクトリではありません。 migrate.invalid_local_path=ローカルパスが無効です。存在しないかディレクトリではありません。
forked_from=フォーク元 forked_from=フォーク元
fork_from_self=すでにあなたの所有しているリポジトリはフォークできません fork_from_self=すでにあなたの所有しているリポジトリはフォークできません
copy_link=コピー copy_link=コピー
copy_link_success=Copied! copy_link_success=コピーされました!
copy_link_error=Press ⌘-C or Ctrl-C to copy copy_link_error=⌘ C または Ctrl-C キーを押してコピー
click_to_copy=クリップボードにコピー click_to_copy=クリップボードにコピー
copied=コピー成功 copied=コピー成功
clone_helper=クローニングのヘルプが必要ですか?<a target="_blank"href="%s"> ヘルプ</a> を参照してください! clone_helper=クローニングのヘルプが必要ですか?<a target="_blank"href="%s"> ヘルプ</a> を参照してください!
@ -380,7 +382,7 @@ quick_guide=クイック ガイド
clone_this_repo=このリポジトリのクローンを作成 clone_this_repo=このリポジトリのクローンを作成
create_new_repo_command=コマンドラインで新しいリポジトリを作成します。 create_new_repo_command=コマンドラインで新しいリポジトリを作成します。
push_exist_repo=コマンド ・ ラインから既存のリポジトリをプッシュ push_exist_repo=コマンド ・ ラインから既存のリポジトリをプッシュ
repo_is_empty=This repository is empty, please come back later! repo_is_empty=このリポジトリは空です、後で戻って来て下さい!
branch=ブランチ branch=ブランチ
@ -431,7 +433,7 @@ issues.filter_label_no_select=選択したラベルがありません。
issues.filter_milestone=マイルストーン issues.filter_milestone=マイルストーン
issues.filter_milestone_no_select=選択されたマイルストーンなし issues.filter_milestone_no_select=選択されたマイルストーンなし
issues.filter_assignee=アサインされた人 issues.filter_assignee=アサインされた人
issues.filter_assginee_no_select=No selected Assignee issues.filter_assginee_no_select=選択可能な担当者がいない
issues.filter_type=タイプ issues.filter_type=タイプ
issues.filter_type.all_issues=すべての問題 issues.filter_type.all_issues=すべての問題
issues.filter_type.assigned_to_you=あなたに割り当てられました。 issues.filter_type.assigned_to_you=あなたに割り当てられました。
@ -440,35 +442,35 @@ issues.filter_type.mentioning_you=あなたに伝える
issues.filter_sort=並べ替え issues.filter_sort=並べ替え
issues.filter_sort.latest=最新 issues.filter_sort.latest=最新
issues.filter_sort.oldest=最も古い issues.filter_sort.oldest=最も古い
issues.filter_sort.recentupdate=Recently updated issues.filter_sort.recentupdate=最近更新された
issues.filter_sort.leastupdate=Least recently updated issues.filter_sort.leastupdate=Least recently updated
issues.filter_sort.mostcomment=Most commented issues.filter_sort.mostcomment=一番多いコメント
issues.filter_sort.leastcomment=Least commented issues.filter_sort.leastcomment=一番少ないコメント
issues.opened_by=opened %[1]s by <a href="%[2]s">%[3]s</a> issues.opened_by=opened %[1]s by <a href="%[2]s">%[3]s</a>
issues.opened_by_fake=opened %[1]s by %[2]s issues.opened_by_fake=opened %[1]s by %[2]s
issues.previous=前ページ issues.previous=前ページ
issues.next=次ページ issues.next=次ページ
issues.open_title=Open issues.open_title=オープン
issues.closed_title=Closed issues.closed_title=クローズ
issues.num_comments=%d comments issues.num_comments=%d コメント
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=There is no content yet. issues.no_content=まだコンテンツがありません
issues.close_issue=Close issues.close_issue=閉じる
issues.close_comment_issue=Close and comment issues.close_comment_issue=コメントと閉じる
issues.reopen_issue=Reopen issues.reopen_issue=Reopen
issues.reopen_comment_issue=Reopen and comment issues.reopen_comment_issue=コメントと再開
issues.create_comment=Comment issues.create_comment=コメント 
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.poster=Poster issues.poster=ポスター
issues.admin=Admin issues.admin=アドミン
issues.owner=Owner issues.owner=オーナー
issues.sign_up_for_free=Sign up for free issues.sign_up_for_free=無料でサインアップ
issues.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a> issues.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a>
issues.edit=Edit issues.edit=編集
issues.cancel=Cancel issues.cancel=キャンセル
issues.save=Save issues.save=保存
issues.label_title=ラベル名 issues.label_title=ラベル名
issues.label_color=ラベルの色 issues.label_color=ラベルの色
issues.label_count=%d ラベル issues.label_count=%d ラベル
@ -482,26 +484,28 @@ issues.label_deletion_success=ラベルは正常に削除されました。
pulls.compare_changes=変更を比較 pulls.compare_changes=変更を比較
pulls.compare_changes_desc=2つのブランチを比較し、プルリクエストを作成します。 pulls.compare_changes_desc=2つのブランチを比較し、プルリクエストを作成します。
pulls.compare_base=base pulls.compare_base=ベース
pulls.compare_compare=compare pulls.compare_compare=比較
pulls.filter_branch=Filter branch pulls.filter_branch=フィルターブランチ
pulls.no_results=結果が見つかりませんでした。 pulls.no_results=結果が見つかりませんでした。
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even. pulls.nothing_to_compare=There is nothing to compare because base and head branches are even.
pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>` pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
pulls.create=Create Pull Request pulls.create=プルリクエストを作成します。
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code>
pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
pulls.tab_conversation=Conversation pulls.tab_conversation=会話
pulls.tab_commits=Commits pulls.tab_commits=コミット
pulls.tab_files=Files changed pulls.tab_files=ファイルが変更された
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation. pulls.reopen_to_merge=Please reopen this pull request to perform merge operation.
pulls.merged=Merged pulls.merged=マージされた
pulls.has_merged=This pull request has been merged successfully! pulls.has_merged=このプルプルリクエストは正常にマージされました!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information. pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request. pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits. pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it. pulls.cannot_auto_merge_helper=それを解決するためにコマンド ライン ツールを使用してください。
pulls.merge_pull_request=Merge Pull Request pulls.merge_pull_request=プルリクエストをマージします。
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
milestones.new=新しいマイルストーン milestones.new=新しいマイルストーン
milestones.open_tab=%d オープン milestones.open_tab=%d オープン
@ -510,22 +514,22 @@ milestones.closed=%s を閉じました
milestones.no_due_date=期限なし milestones.no_due_date=期限なし
milestones.open=開く milestones.open=開く
milestones.close=閉じる milestones.close=閉じる
milestones.new_subheader=Create milestones to organize your issues. milestones.new_subheader=あなたの課題を整理するためマイルス トーンを作成します。
milestones.create=Create Milestone milestones.create=マイルス トーンを作成
milestones.title=Title milestones.title=タイトル
milestones.desc=Description milestones.desc=説明
milestones.due_date=Due Date (optional) milestones.due_date=期日 (オプション)
milestones.clear=Clear milestones.clear=消去
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'. milestones.invalid_due_date_format=期限日付のフォーマットが無効、'yyyy-mm-dd' のフォーマットが必要です。
milestones.create_success=Milestone '%s' has been created successfully! milestones.create_success=マイルス トーン '%s' が正常に作成されました!
milestones.edit=Edit Milestone milestones.edit=マイルス トーンを編集
milestones.edit_subheader=Use better description for milestones so people won't be confused. milestones.edit_subheader=人々を混乱させないため、マイルス トーンにより良い説明を使用します。
milestones.cancel=Cancel milestones.cancel=キャンセル
milestones.modify=Modify Milestone milestones.modify=マイルス トーンを変更します。
milestones.edit_success=Changes of milestone '%s' has been saved successfully! milestones.edit_success=マイルス トーン '%s' の変更が正常に保存されました。
milestones.deletion=Milestone Deletion milestones.deletion=マイルス トーンの削除
milestones.deletion_desc=Delete this milestone will remove its information in all related issues. Do you want to continue? milestones.deletion_desc=このマイルス トーンを削除すると、関連課題に該当情報が削除されます。続行しますか。
milestones.deletion_success=Milestone has been deleted successfully! milestones.deletion_success=マイルス トーンは正常に削除されました。
settings=設定 settings=設定
settings.options=オプション settings.options=オプション
@ -536,15 +540,15 @@ settings.basic_settings=基本設定
settings.danger_zone=危険地帯 settings.danger_zone=危険地帯
settings.site=公式サイト settings.site=公式サイト
settings.update_settings=設定の更新 settings.update_settings=設定の更新
settings.change_reponame_prompt=This change will affect how links relate to the repository. settings.change_reponame_prompt=この変更はリンクがリポジトリに関連付ける方法に影響します。
settings.transfer=オーナー移転 settings.transfer=オーナー移転
settings.transfer_desc=リポジトリをあなたが管理者権限を持っている別のユーザーまた組織に移譲します。 settings.transfer_desc=リポジトリをあなたが管理者権限を持っている別のユーザーまた組織に移譲します。
settings.new_owner_has_same_repo=新しいオーナーは、既に同じ名前のリポジトリを持っています。 settings.new_owner_has_same_repo=新しいオーナーは、既に同じ名前のリポジトリを持っています。
settings.delete=このリポジトリを削除 settings.delete=このリポジトリを削除
settings.delete_desc=リポジトリを削除すると元に戻せません。確実に確認してください。 settings.delete_desc=リポジトリを削除すると元に戻せません。確実に確認してください。
settings.transfer_notices_1=- You will lose access if new owner is a individual user. settings.transfer_notices_1=-新しい所有者が個人ユーザーの場合、あなたがアクセスできなくなります。
settings.transfer_notices_2=- You will conserve access if new owner is an organization and if you're one of the owners. settings.transfer_notices_2=- You will conserve access if new owner is an organization and if you're one of the owners.
settings.transfer_form_title=Please enter following information to confirm your operation: settings.transfer_form_title=操作を確認するために、以下の情報を入力してください。
settings.delete_notices_1=- This operation <strong>CANNOT</strong> be undone. settings.delete_notices_1=- This operation <strong>CANNOT</strong> be undone.
settings.delete_notices_2=- This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators. settings.delete_notices_2=- This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators.
settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion. settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion.
@ -561,14 +565,14 @@ settings.remove_collaborator_success=共同編集者が削除されました。
settings.user_is_org_member=ユーザーは組織の一員なので、共同編集者として追加することはできません。 settings.user_is_org_member=ユーザーは組織の一員なので、共同編集者として追加することはできません。
settings.add_webhook=Webhook を追加 settings.add_webhook=Webhook を追加
settings.hooks_desc=Webhooksは、Gogsで特定のイベントの発生時に指定された外部サービスに通知を許可します。イベントが発生すると、それぞれ指定されたUrlに、POSTリクエストが送られます。詳細はこちらのの <a target="_blank"href="%s"> Webhooks ガイド</a>をご覧ください。 settings.hooks_desc=Webhooksは、Gogsで特定のイベントの発生時に指定された外部サービスに通知を許可します。イベントが発生すると、それぞれ指定されたUrlに、POSTリクエストが送られます。詳細はこちらのの <a target="_blank"href="%s"> Webhooks ガイド</a>をご覧ください。
settings.webhook_deletion=Delete Webhook settings.webhook_deletion=Webhook を削除
settings.webhook_deletion_desc=Delete this webhook will remove its information and all delivery history. Do you want to continue? settings.webhook_deletion_desc=このwebhookを削除すると、すべての情報と配信履歴が削除されます。続行しますか?
settings.webhook_deletion_success=Webhook has been deleted successfully! settings.webhook_deletion_success=Webhook が正常に削除されました。
settings.webhook.request=Request settings.webhook.request=リクエスト
settings.webhook.response=Response settings.webhook.response=レスポンス
settings.webhook.headers=Headers settings.webhook.headers=ヘッダ
settings.webhook.payload=Payload settings.webhook.payload=ペイロード
settings.webhook.body=Body settings.webhook.body=ボディ
settings.githooks_desc=Git のフックは Git 自体によって提供されています。以下のリストのファイルを編集して、サポートされているフックのカスタム操作を適用することができます。 settings.githooks_desc=Git のフックは Git 自体によって提供されています。以下のリストのファイルを編集して、サポートされているフックのカスタム操作を適用することができます。
settings.githook_edit_desc=もしフックがアクティブではない場合は、サンプルコンテンツが表示されます。コンテンツを空白にするにはこのフックを無効にします。 settings.githook_edit_desc=もしフックがアクティブではない場合は、サンプルコンテンツが表示されます。コンテンツを空白にするにはこのフックを無効にします。
settings.githook_name=フックの名前 settings.githook_name=フックの名前
@ -578,17 +582,17 @@ settings.add_webhook_desc=私たちは、指定されたURLに購読されたイ
settings.payload_url=ペイロードの URL settings.payload_url=ペイロードの URL
settings.content_type=コンテンツ タイプ settings.content_type=コンテンツ タイプ
settings.secret=秘密 settings.secret=秘密
settings.slack_username=Username settings.slack_username=ユーザ名
settings.slack_icon_url=Icon URL settings.slack_icon_url=アイコン URL
settings.slack_color=Color settings.slack_color=カラー
settings.event_desc=どのイベントをこのWEBフックのトリガーにしますか? settings.event_desc=どのイベントをこのWEBフックのトリガーにしますか?
settings.event_push_only=<code>push</code> イベントのみ settings.event_push_only=<code>push</code> イベントのみ
settings.event_send_everything=I need <strong>everything</strong>. settings.event_send_everything=<strong>すべて</strong> が必要です。
settings.event_choose=Let me choose what I need. settings.event_choose=必要なものを選択しましょう。
settings.event_create=Create settings.event_create=Create
settings.event_create_desc=Branch, or tag created settings.event_create_desc=ブランチ、またはタグを作成
settings.event_push=Push settings.event_push=プッシュ
settings.event_push_desc=Git push to a repository settings.event_push_desc=Git リポジトリにプッシュ
settings.active=アクティブ settings.active=アクティブ
settings.active_helper=このフックのトリガーが引かれた時に、イベントの詳細を配信します。 settings.active_helper=このフックのトリガーが引かれた時に、イベントの詳細を配信します。
settings.add_hook_success=新しい webhook が追加されました。 settings.add_hook_success=新しい webhook が追加されました。
@ -602,16 +606,16 @@ settings.slack_token=トークン
settings.slack_domain=ドメイン settings.slack_domain=ドメイン
settings.slack_channel=チャンネル settings.slack_channel=チャンネル
settings.deploy_keys=デプロイキー settings.deploy_keys=デプロイキー
settings.add_deploy_key=Add Deploy Key settings.add_deploy_key=デプロイキーを追加
settings.no_deploy_keys=You haven't added any deploy key. settings.no_deploy_keys=でプロキーは1つも追加されていません。
settings.title=Title settings.title=タイトル
settings.deploy_key_content=Content settings.deploy_key_content=コンテント
settings.key_been_used=Deploy key content has been used. settings.key_been_used=デプロイキーが使用されています。
settings.key_name_used=Deploy key with same name has already existed. settings.key_name_used=同じ名前のデプロイキーが既に存在しています。
settings.add_key_success=New deploy key '%s' has been added successfully! settings.add_key_success=新しいデプロイキー '%s'が正常に追加されました!
settings.deploy_key_deletion=Delete Deploy Key settings.deploy_key_deletion=デプロイキーを削除
settings.deploy_key_deletion_desc=Delete this deploy key will remove all related accesses for this repository. Do you want to continue? settings.deploy_key_deletion_desc=このデプロイキーを削除すると、このリポジトリに関連するすべてのアクセス権も削除されます。続行しますか。
settings.deploy_key_deletion_success=Deploy key has been deleted successfully! settings.deploy_key_deletion_success=デプロイキーが正常に削除された!
diff.browse_source=ソースを参照 diff.browse_source=ソースを参照
diff.parent= diff.parent=
@ -648,6 +652,7 @@ release.tag_name_already_exist=このタグ名には既にリリースが存在
[org] [org]
org_name_holder=組織名 org_name_holder=組織名
org_full_name_holder=組織のフルネーム
org_name_helper=偉大な組織の名は短く覚えやすいです。 org_name_helper=偉大な組織の名は短く覚えやすいです。
create_org=組織を作成 create_org=組織を作成
repo_updated=更新した repo_updated=更新した
@ -733,7 +738,7 @@ notices=システム通知
monitor=モニタリング monitor=モニタリング
first_page=First first_page=First
last_page=Last last_page=Last
total=Total: %d total=合計: %d
dashboard.statistic=統計 dashboard.statistic=統計
dashboard.operations=操作 dashboard.operations=操作
@ -792,23 +797,24 @@ users.activated=アクティブ化
users.admin=アドミン users.admin=アドミン
users.repos=リポジトリ users.repos=リポジトリ
users.created=作成されました users.created=作成されました
users.send_register_notify=Send Registration Notification To User users.send_register_notify=登録通知をユーザーに送信
users.new_success=New account '%s' has been created successfully. users.new_success=New account '%s' has been created successfully.
users.edit=編集 users.edit=編集
users.auth_source=Authentication Source users.auth_source=認証ソース
users.local=ローカル users.local=ローカル
users.auth_login_name=Authentication Login Name users.auth_login_name=認証ログイン名
users.password_helper=Leave it empty to remain unchanged. users.password_helper=それをそのまま空のままにします。
users.update_profile_success=アカウントのプロファイルが更新されました。 users.update_profile_success=アカウントのプロファイルが更新されました。
users.edit_account=アカウントの編集 users.edit_account=アカウントの編集
users.is_activated=アカウントがアクティブされました users.is_activated=アカウントがアクティブされました
users.is_admin=このアカウントには管理者の権限を持つ users.is_admin=このアカウントには管理者の権限を持つ
users.allow_git_hook=このアカウントには Git のフックを作成する権限を持つ users.allow_git_hook=このアカウントには Git のフックを作成する権限を持つ
users.allow_import_local=This account has permissions to import local repositories
users.update_profile=アカウント ・ プロファイルを更新 users.update_profile=アカウント ・ プロファイルを更新
users.delete_account=このアカウントを削除 users.delete_account=このアカウントを削除
users.still_own_repo=アカウント所有のリポジトリがあり、リポジトリの削除または所有者の移譲が必要です。 users.still_own_repo=アカウント所有のリポジトリがあり、リポジトリの削除または所有者の移譲が必要です。
users.still_has_org=アカウントはまだ組織のメンバーであり、組織から退出するか削除する必要があります。 users.still_has_org=アカウントはまだ組織のメンバーであり、組織から退出するか削除する必要があります。
users.deletion_success=Account has been deleted successfully! users.deletion_success=アカウントが正常に削除されました。
orgs.org_manage_panel=組織の管理パネル orgs.org_manage_panel=組織の管理パネル
orgs.name=名前 orgs.name=名前
@ -823,47 +829,47 @@ repos.watches=Watches
repos.stars=Stars repos.stars=Stars
repos.issues=課題 repos.issues=課題
auths.auth_manage_panel=Authentication Manage Panel auths.auth_manage_panel=認証管理パネル
auths.new=Add New Source auths.new=新しいソースを追加
auths.name=名前 auths.name=名前
auths.type=タイプ auths.type=タイプ
auths.enabled=Enabled auths.enabled=Enabled
auths.updated=Updated auths.updated=Updated
auths.auth_type=Authentication Type auths.auth_type=認証タイプ
auths.auth_name=Authentication Name auths.auth_name=認証名
auths.domain=ドメイン auths.domain=ドメイン
auths.host=ホスト auths.host=ホスト
auths.port=ポート auths.port=ポート
auths.bind_dn=Bind DN auths.bind_dn=バインド DN
auths.bind_password=Bind Password auths.bind_password=バインド パスワード
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account. auths.bind_password_helper=警告: このパスワードは暗号化されずに格納されます。特権を持つアカウントに使用しないでください。
auths.user_base=User Search Base auths.user_base=ユーザ検索ベース
auths.user_dn=User DN auths.user_dn=User DN
auths.attribute_name=名前属性 auths.attribute_name=名前属性
auths.attribute_surname=名字属性 auths.attribute_surname=名字属性
auths.attribute_mail=Eメール属性 auths.attribute_mail=Eメール属性
auths.filter=User Filter auths.filter=User フィルター
auths.admin_filter=Admin Filter auths.admin_filter=Admin フィルター
auths.ms_ad_sa=Ms Ad SA auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=SMTP Authentication Type auths.smtp_auth=SMTP 認証の種類
auths.smtphost=SMTP ホスト auths.smtphost=SMTP ホスト
auths.smtpport=SMTP ポート auths.smtpport=SMTP ポート
auths.allowed_domains=Allowed Domains auths.allowed_domains=許可されているドメイン
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','. auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','.
auths.enable_tls=TLS 暗号化を有効にする auths.enable_tls=TLS 暗号化を有効にする
auths.skip_tls_verify=Skip TLS Verify auths.skip_tls_verify=TLSベリファイを省略
auths.pam_service_name=PAMサービス名 auths.pam_service_name=PAMサービス名
auths.enable_auto_register=自動登録を有効にする auths.enable_auto_register=自動登録を有効にする
auths.tips=ヒント auths.tips=ヒント
auths.edit=Edit Authentication Setting auths.edit=認証設定を編集
auths.activated=認証がアクティブされました auths.activated=認証がアクティブされました
auths.new_success=New authentication '%s' has been added successfully. auths.new_success=新しい認証 '%s' が正常に追加されました。
auths.update_success=Authentication setting has been updated successfully. auths.update_success=認証の設定が正常に更新されました。
auths.update=Update Authentication Setting auths.update=認証設定を更新
auths.delete=Delete This Authentication auths.delete=この認証を削除
auths.delete_auth_title=Authentication Deletion auths.delete_auth_title=認証削除
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue? auths.delete_auth_desc=認証を削除します、継続しますか?
auths.deletion_success=Authentication has been deleted successfully! auths.deletion_success=認証が正常に削除されました。
config.server_config=サーバーの構成 config.server_config=サーバーの構成
config.app_name=アプリケーション名 config.app_name=アプリケーション名
@ -895,12 +901,12 @@ config.show_registration_button=登録ボタンを表示します。
config.require_sign_in_view=サインインを要求 config.require_sign_in_view=サインインを要求
config.enable_cache_avatar=アバターのキャッシュを有効にします。 config.enable_cache_avatar=アバターのキャッシュを有効にします。
config.mail_notify=メール通知 config.mail_notify=メール通知
config.disable_key_size_check=Disable Minimum Key Size Check config.disable_key_size_check=最小キー サイズ チェックを無効にします
config.enable_captcha=Enable Captcha config.enable_captcha=Captchaを有効にする
config.active_code_lives=コードリンクの有効期限をアクティブ config.active_code_lives=コードリンクの有効期限をアクティブ
config.reset_password_code_lives=パスワードリンクの有効期限をリセット config.reset_password_code_lives=パスワードリンクの有効期限をリセット
config.webhook_config=Webhook設定 config.webhook_config=Webhook設定
config.queue_length=Queue Length config.queue_length=キューの長さ
config.deliver_timeout=送信タイムアウト config.deliver_timeout=送信タイムアウト
config.skip_tls_verify=TLSの確認を省略 config.skip_tls_verify=TLSの確認を省略
config.mailer_config=メーラーの構成 config.mailer_config=メーラーの構成
@ -950,12 +956,12 @@ notices.delete_success=システム通知が正常に削除されました。
[action] [action]
create_repo=リポジトリ <a href="%s"> %s</a>を作成しました create_repo=リポジトリ <a href="%s"> %s</a>を作成しました
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a> rename_repo=<code>%[1]s</code> から <a href="%[2]s">[3]s</a> にリポジトリ名を変更した
commit_repo=<a href="%[1]s">%[3]s</a>を<a href="%[1]s/src/%[2]s">%[2]s</a>にプッシュしました commit_repo=<a href="%[1]s">%[3]s</a>を<a href="%[1]s/src/%[2]s">%[2]s</a>にプッシュしました
create_issue=`問題 <a href="%s/issues/%s">%s#%[2]s</a> を開きました` create_issue=`問題 <a href="%s/issues/%s">%s#%[2]s</a> を開きました`
create_pull_request=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>` create_pull_request=`プルリクエスト <a href="%s/pulls/%s"> %s[2]s</a>を作成`
comment_issue=`問題 <a href="%s/issues/%s">%s#%[2]s</a> のコメント` comment_issue=`問題 <a href="%s/issues/%s">%s#%[2]s</a> のコメント`
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>` merge_pull_request=`プルリクエスト <a href="%s/pulls/%s"> %s[2]s</a>をマージしました`
transfer_repo=リポジトリ <code>%s</code> を <a href="%s">%s</a> へ転送しました transfer_repo=リポジトリ <code>%s</code> を <a href="%s">%s</a> へ転送しました
push_tag=<a href="%[1]s">%[3]s</a> に タグ <a href="%[1]s/src/%[2]s">%[2]s</a> をプッシュしました push_tag=<a href="%[1]s">%[3]s</a> に タグ <a href="%[1]s/src/%[2]s">%[2]s</a> をプッシュしました
compare_2_commits=これら 2 のコミットの比較を閲覧する compare_2_commits=これら 2 のコミットの比較を閲覧する
@ -982,8 +988,8 @@ raw_seconds=秒
raw_minutes= raw_minutes=
[dropzone] [dropzone]
default_message=Drop files here or click to upload. default_message=ここにファイルをドロップまたはクリックしてアップロードします。
invalid_input_type=You can't upload files of this type. invalid_input_type=このタイプのファイルはアップロードできません.
file_too_big=File size({{filesize}} MB) exceeds maximum size({{maxFilesize}} MB). file_too_big=ファイルのサイズ ({{filesize}} MB) では、最大サイズ ({{maxFilesize}} MB) を超えています。
remove_file=Remove file remove_file=ファイル削除

504
conf/locale/locale_lv-LV.ini

@ -13,7 +13,7 @@ version=Versija
page=Lapa page=Lapa
template=Sagatave template=Sagatave
language=Valoda language=Valoda
create_new=Create... create_new=Izveidot...
user_profile_and_more=Lietotāja profilu un vairāk user_profile_and_more=Lietotāja profilu un vairāk
signed_in_as=Pierakstījies kā signed_in_as=Pierakstījies kā
@ -53,8 +53,8 @@ code=Kods
[install] [install]
install=Instalācija install=Instalācija
title=Instalācijas soļi pirmo reizi palaižot title=Instalācijas soļi pirmo reizi palaižot
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page! docker_helper=Ja Gogs tiek lietots zem Docker, izlasiet uzmanīgi <a target="_blank" href="%s">vadlīnijas</a>, pirms ko maināt šajā lapā!
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB. requite_db_desc=Gogs nepieciešams MySQL, PostgreSQL, SQLite3 vai TiDB.
db_title=Datu bāzes iestatījumi db_title=Datu bāzes iestatījumi
db_type=Datu bāzes veids db_type=Datu bāzes veids
host=Resursdators host=Resursdators
@ -64,11 +64,11 @@ db_name=Datu bāzes nosaukums
db_helper=Nepieciešams izmantot MySQL INNODB dzini ar rakstzīmju kopu utf8_general_ci. db_helper=Nepieciešams izmantot MySQL INNODB dzini ar rakstzīmju kopu utf8_general_ci.
ssl_mode=SSL režīms ssl_mode=SSL režīms
path=Ceļš path=Ceļš
sqlite_helper=The file path of SQLite3 or TiDB database. sqlite_helper=SQLite3 vai TiDB datu bāzes faila atrašanās vieta.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty. err_empty_db_path=Nepieciešams norādīt SQLite3 vai TiDB datu bāzes atrašanās vietu.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-". err_invalid_tidb_name=TiDB datu bāzes nosaukums nevar saturēt simbolus "." un "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account. no_admin_and_disable_registration=Reģistrāciju nevar atslēgt, kamēr nav izveidots administratora konts.
err_empty_admin_password=Admin password cannot be empty. err_empty_admin_password=Administratora kontam ir obligāti jānorāda parole.
general_title=Gogs vispārīgie iestatījumi general_title=Gogs vispārīgie iestatījumi
app_name=Lietotnes nosaukums app_name=Lietotnes nosaukums
@ -79,8 +79,8 @@ run_user=Izpildes lietotājs
run_user_helper=Lietotājam ir jābūt rakstīšanas tiesībām repozitorija saknes direktorijai un Gogs jābūt palaistam zem šī lietotāja. run_user_helper=Lietotājam ir jābūt rakstīšanas tiesībām repozitorija saknes direktorijai un Gogs jābūt palaistam zem šī lietotāja.
domain=Domēns domain=Domēns
domain_helper=Tas ietekmē SSH klonēšanas URL. domain_helper=Tas ietekmē SSH klonēšanas URL.
ssh_port=SSH Port ssh_port=SSH ports
ssh_port_helper=Port number which your SSH server is using, leave it empty to disable SSH feature. ssh_port_helper=Porta numurs, kuru izmanto Jūsu SSH serveris, atstājiet tukšu, ja nevēlaties izmantot SSH.
http_port=HTTP ports http_port=HTTP ports
http_port_helper=Porta numurs pēc kura lietojumprogrammai būs iespējams pieslēgties. http_port_helper=Porta numurs pēc kura lietojumprogrammai būs iespējams pieslēgties.
app_url=Lietotnes URL app_url=Lietotnes URL
@ -98,12 +98,12 @@ mail_notify=Iespējot e-pasta paziņojumus
server_service_title=Servera un citu servisu iestatījumi server_service_title=Servera un citu servisu iestatījumi
offline_mode=Iespējot bezsaistes režīmu offline_mode=Iespējot bezsaistes režīmu
offline_mode_popup=Atspējot CDN arī produkcijas režīmā, visi resursu faili tiks piegādāti no servera. offline_mode_popup=Atspējot CDN arī produkcijas režīmā, visi resursu faili tiks piegādāti no servera.
disable_gravatar=Disable Gravatar Service disable_gravatar=Atspējot Gravatar pakalpojumu
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default. disable_gravatar_popup=Atspējot Gravatar un citus avotus, visus avatarus augšupielādēts lietotāji vai izmantos noklusēto attēlu.
disable_registration=Atspējot lietotāju reģistrāciju disable_registration=Atspējot lietotāju reģistrāciju
disable_registration_popup=Atspējot lietotāju reģistrāciju, tikai administrators varēs izveidot jaunus lietotāju kontus. disable_registration_popup=Atspējot lietotāju reģistrāciju, tikai administrators varēs izveidot jaunus lietotāju kontus.
enable_captcha=Enable Captcha enable_captcha=Iespējot drošības kodu
enable_captcha_popup=Require validate captcha for user self-registration. enable_captcha_popup=Lietotājam reģistrējoties, pieprasīt ievadīt drošības kodu.
require_sign_in_view=Iespējot nepieciešamību autorizēties, lai aplūkotu lapas require_sign_in_view=Iespējot nepieciešamību autorizēties, lai aplūkotu lapas
require_sign_in_view_popup=Tika autorizēti lietotāji var aplūkot lapas, neautorizēti lietotāji var piekļūt tikai autorizācijas un reģistrēšanās lapām. require_sign_in_view_popup=Tika autorizēti lietotāji var aplūkot lapas, neautorizēti lietotāji var piekļūt tikai autorizācijas un reģistrēšanās lapām.
admin_setting_desc=Nav nepieciešams izveidot administratora kontu uzreiz, lietotājs ar ID=1 saņems administratora tiesības automātiski. admin_setting_desc=Nav nepieciešams izveidot administratora kontu uzreiz, lietotājs ar ID=1 saņems administratora tiesības automātiski.
@ -111,7 +111,7 @@ admin_title=Admin konta iestatījumi
admin_name=Lietotājvārds admin_name=Lietotājvārds
admin_password=Parole admin_password=Parole
confirm_password=Apstipriniet paroli confirm_password=Apstipriniet paroli
admin_email=E-pasts admin_email=Administratora e-pasts
install_gogs=Instalēt Gogs install_gogs=Instalēt Gogs
test_git_failed=Kļūda pārbaudot 'git' komandu: %v test_git_failed=Kļūda pārbaudot 'git' komandu: %v
sqlite3_not_available=Jūsu versija neatbalsta SQLite3, lūdzu lejupielādējiet oficiālo bināro versiju no %s, NEVIS gobuild versiju. sqlite3_not_available=Jūsu versija neatbalsta SQLite3, lūdzu lejupielādējiet oficiālo bināro versiju no %s, NEVIS gobuild versiju.
@ -130,9 +130,9 @@ my_repos=Mani repozitoriji
collaborative_repos=Sadarbības repozitoriji collaborative_repos=Sadarbības repozitoriji
my_orgs=Manas organizācijas my_orgs=Manas organizācijas
my_mirrors=Mani spoguļi my_mirrors=Mani spoguļi
view_home=View %s view_home=Skatīties %s
issues.in_your_repos=In your repositories issues.in_your_repos=Jūsu repozitorijos
[explore] [explore]
repos=Repozitoriji repos=Repozitoriji
@ -148,7 +148,6 @@ forgot_password=Aizmirsu paroli
forget_password=Aizmirsi paroli? forget_password=Aizmirsi paroli?
sign_up_now=Nepieciešams konts? Reģistrējies tagad. sign_up_now=Nepieciešams konts? Reģistrējies tagad.
confirmation_mail_sent_prompt=Jauns apstiprināšanas e-pasts ir nosūtīts uz <b>%s</b>, lūdzu, pārbaudies savu e-pasta kontu tuvāko %d stundu laikā, lai pabeigtu reģistrācijas procesu. confirmation_mail_sent_prompt=Jauns apstiprināšanas e-pasts ir nosūtīts uz <b>%s</b>, lūdzu, pārbaudies savu e-pasta kontu tuvāko %d stundu laikā, lai pabeigtu reģistrācijas procesu.
sign_in_to_account=Sign in to your account
active_your_account=Aktivizēt savu kontu active_your_account=Aktivizēt savu kontu
resent_limit_prompt=Atvainojiet, Jūs sūtījāt aktivizācijas e-pastu pārāk bieži. Lūdzu, gaidiet 3 minūtes. resent_limit_prompt=Atvainojiet, Jūs sūtījāt aktivizācijas e-pastu pārāk bieži. Lūdzu, gaidiet 3 minūtes.
has_unconfirmed_mail=Sveiki %s, Jums ir neapstiprināta e-pasta adrese (<b>%s</b>). Ja neesat saņēmis apstiprināšanas e-pastu vai Jums ir nepieciešams nosūtīt jaunu, lūdzu, nospiediet pogu, kas atrodas zemāk. has_unconfirmed_mail=Sveiki %s, Jums ir neapstiprināta e-pasta adrese (<b>%s</b>). Ja neesat saņēmis apstiprināšanas e-pastu vai Jums ir nepieciešams nosūtīt jaunu, lūdzu, nospiediet pogu, kas atrodas zemāk.
@ -161,10 +160,10 @@ reset_password_helper=Nospiediet šeit, lai atjaunotu paroli
password_too_short=Paroles garums nedrīkst būt mazāks par 6. password_too_short=Paroles garums nedrīkst būt mazāks par 6.
[mail] [mail]
activate_account=Please activate your account activate_account=Lūdzu, aktivizējiet savu kontu
activate_email=Verify your e-mail address activate_email=Apstipriniet savu e-pasta adresi
reset_password=Reset your password reset_password=Atiestatīt savu paroli
register_success=Register success, Welcome register_success=Reģistrācija notikusi veiksmīgi
[modal] [modal]
yes= yes=
@ -192,6 +191,7 @@ min_size_error=` jabūt vismaz %s simbolu garumā.`
max_size_error=` jabūt ne mazāk kā %s simbolu garumā.` max_size_error=` jabūt ne mazāk kā %s simbolu garumā.`
email_error=` nav derīga e-pasta adrese.` email_error=` nav derīga e-pasta adrese.`
url_error=` nav korekts URL.` url_error=` nav korekts URL.`
include_error=` ir jāsatur tekstu '%s'.`
unknown_error=Nezināma kļūda: unknown_error=Nezināma kļūda:
captcha_incorrect=Pārbaudes kods nesakrīt ar attēlā redzamo. captcha_incorrect=Pārbaudes kods nesakrīt ar attēlā redzamo.
password_not_match=Parole un atkārtoti ievadītā parole nav vienādas. password_not_match=Parole un atkārtoti ievadītā parole nav vienādas.
@ -252,7 +252,7 @@ location=Atrašanās vieta
update_profile=Mainīt profilu update_profile=Mainīt profilu
update_profile_success=Jūsu profila dati ir veiksmīgi saglabāti. update_profile_success=Jūsu profila dati ir veiksmīgi saglabāti.
change_username=Lietotāja vārds mainīts change_username=Lietotāja vārds mainīts
change_username_prompt=This change will affect the way how links relate to your account. change_username_prompt=Šī izmaiņa ietekmēs saites, kas norāda uz Jūsu kontu.
continue=Turpināt continue=Turpināt
cancel=Atcelt cancel=Atcelt
@ -267,7 +267,7 @@ update_avatar_success=Jūsu profila bilde tika veiksmīgi saglabāta.
change_password=Mainīt paroli change_password=Mainīt paroli
old_password=Pašreizējā parole old_password=Pašreizējā parole
new_password=Jauna parole new_password=Jauna parole
retype_new_password=Retype New Password retype_new_password=Ievadīt paroli atkāroti
password_incorrect=Ievadīta nepareiza pašreizējā parole. password_incorrect=Ievadīta nepareiza pašreizējā parole.
change_password_success=Parole tika veiksmīgi nomainīta. Tagad jūs varat pieraksītites, izmantojot jauno paroli. change_password_success=Parole tika veiksmīgi nomainīta. Tagad jūs varat pieraksītites, izmantojot jauno paroli.
@ -277,12 +277,12 @@ email_desc=Primārā e-pasta adrese tiks izmantota sūtot notifikācijas un cit
primary=Primārā primary=Primārā
primary_email=Iestatīt kā primāro primary_email=Iestatīt kā primāro
delete_email=Dzēst delete_email=Dzēst
email_deletion=E-mail Deletion email_deletion=E-pasta dzēšana
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue? email_deletion_desc=Dzēšot šo e-pasta adresi, tiks dzēsta arī visa ar to saistītā informācija no Jūsu konta. Vai vēlaties turpināt?
email_deletion_success=E-mail has been deleted successfully! email_deletion_success=E-pasta adrese ir veiksmīgi izdzēsta!
add_new_email=Pievienot jaunu e-pasta adresi add_new_email=Pievienot jaunu e-pasta adresi
add_email=Pievienot e-pastu add_email=Pievienot e-pastu
add_email_confirmation_sent=A new confirmation e-mail has been sent to '%s', please check your inbox within the next %d hours to complete the confirmation process. add_email_confirmation_sent=Jauns apstiprinājuma e-pasts tika nosūtīts uz '%s', pārbaudiet savu e-pastu tuvāko %d stundu laikā, lai pabeigtu apstiprināšanas procesu.
add_email_success=Jūsu jaunā e-pasta adrese tika veiksmīgi pievienota. add_email_success=Jūsu jaunā e-pasta adrese tika veiksmīgi pievienota.
manage_ssh_keys=Pārvaldīt SSH atslēgas manage_ssh_keys=Pārvaldīt SSH atslēgas
@ -290,20 +290,20 @@ add_key=Pievienot atslēgu
ssh_desc=Šis ir saraksts ar Jūsu kontam piesaistītajām SSH atslēgām. Dzēsiet visas, kuras Jūs neatpazīstat. ssh_desc=Šis ir saraksts ar Jūsu kontam piesaistītajām SSH atslēgām. Dzēsiet visas, kuras Jūs neatpazīstat.
ssh_helper=<strong>Vajadzīga palīdzība?</strong> Apskatieties pamācību kā <a href="%s">ģenerēt SSH atslēgas</a> vai kā novērst <a href="%s">biežāk sastopamās SSH problēmas</a>. ssh_helper=<strong>Vajadzīga palīdzība?</strong> Apskatieties pamācību kā <a href="%s">ģenerēt SSH atslēgas</a> vai kā novērst <a href="%s">biežāk sastopamās SSH problēmas</a>.
add_new_key=Pievienot SSH atslēgu add_new_key=Pievienot SSH atslēgu
ssh_key_been_used=Public key content has been used. ssh_key_been_used=Šī publiskā atslēga jau ir izmantota.
ssh_key_name_used=Public key with same name has already existed. ssh_key_name_used=Publiskā atslēga ar šādu nosaukumu jau eksistē.
key_name=Atslēgas nosaukums key_name=Atslēgas nosaukums
key_content=Saturs key_content=Saturs
add_key_success=New SSH key '%s' has been added successfully! add_key_success=Jauna SSH atslēga '%s' tika veiksmīgi pievienota!
delete_key=Dzēst delete_key=Dzēst
ssh_key_deletion=SSH Key Deletion ssh_key_deletion=SSH atslēgas dzēšana
ssh_key_deletion_desc=Delete this SSH key will remove all related accesses for your account. Do you want to continue? ssh_key_deletion_desc=Dzēšot šo SSH atslēgu, tiks dzēsta visa ar to saistītā piekļuve Jūsu kontam. Vai vēlaties turpināt?
ssh_key_deletion_success=SSH key has been deleted successfully! ssh_key_deletion_success=SSH atslēga tika veiksmīgi izdzēsta!
add_on=Pievienota add_on=Pievienota
last_used=Pēdējo reizi izmantota last_used=Pēdējo reizi izmantota
no_activity=Nav nesenas aktivitātes no_activity=Nav nesenas aktivitātes
key_state_desc=This key is used in last 7 days key_state_desc=Šī atslēga tika izmantota pēdējo 7 dienu laikā
token_state_desc=This token is used in last 7 days token_state_desc=Šis talons tika izmantots pēdējo 7 dienu laikā
manage_social=Pārvaldīt piesaistītos sociālos kontus manage_social=Pārvaldīt piesaistītos sociālos kontus
social_desc=Šeit tiek attēloti visi sociālie konti, kas ir piesaistīti Jūsu kontam. Dzēsiet visus, kurus Jūs neatpazīstat. social_desc=Šeit tiek attēloti visi sociālie konti, kas ir piesaistīti Jūsu kontam. Dzēsiet visus, kurus Jūs neatpazīstat.
@ -312,15 +312,15 @@ unbind_success=Sociālais konts tika atsaistīts.
manage_access_token=Pārvaldīt personīgos piekļuves talonus manage_access_token=Pārvaldīt personīgos piekļuves talonus
generate_new_token=Ģenerēt jaunu talonu generate_new_token=Ģenerēt jaunu talonu
tokens_desc=Tokens you have generated that can be used to access the Gogs APIs. tokens_desc=Taloni, kurus esat uzģenerējuši, kas var tikt izmantoti, lai piekļūtu Gogs API.
new_token_desc=Pašlaik visiem taloniem ir pilna piekļuve Jūsu kontam. new_token_desc=Pašlaik visiem taloniem ir pilna piekļuve Jūsu kontam.
token_name=Talona nosaukums token_name=Talona nosaukums
generate_token=Ģenerēt talonu generate_token=Ģenerēt talonu
generate_token_succees=Jauns piekļuves talons tika veiksmīgi uzģenerēts! Pārliecinieties, ka esat to nokopējis, jo to Jums vairāk nebūs iespēja izdarīt! generate_token_succees=Jauns piekļuves talons tika veiksmīgi uzģenerēts! Pārliecinieties, ka esat to nokopējis, jo to Jums vairāk nebūs iespēja izdarīt!
delete_token=Dzēst delete_token=Dzēst
access_token_deletion=Personal Access Token Deletion access_token_deletion=Personīgā piekļuves talona dzēšana
access_token_deletion_desc=Delete this personal access token will remove all related accesses of application. Do you want to continue? access_token_deletion_desc=Dzēšot personīgo piekļuves talonu, tiks liegta piekļuve aplikācijām, kas to izmanto. Vai vēlaties turpināt?
delete_token_success=Personal access token has been removed successfully! Don't forget to update your application as well. delete_token_success=Personīgās piekļuves talons veiksmīgi izdzēsts! Neaizmirstiet nomainīt uz citu aplikācijās, kas to izmantoja.
delete_account=Dzēst savu kontu delete_account=Dzēst savu kontu
delete_prompt=Šī darbība pilnībā izdzēsīs Jūsu kontu, kā arī tā ir <strong>NEATGRIEZENISKA</strong>! delete_prompt=Šī darbība pilnībā izdzēsīs Jūsu kontu, kā arī tā ir <strong>NEATGRIEZENISKA</strong>!
@ -333,39 +333,41 @@ owner=Īpašnieks
repo_name=Repozitorija nosaukums repo_name=Repozitorija nosaukums
repo_name_helper=Labi repzotoriju nosaukumi ir īsi, tādi kurus viegli atcerēties un <strong>unikāli</strong>. repo_name_helper=Labi repzotoriju nosaukumi ir īsi, tādi kurus viegli atcerēties un <strong>unikāli</strong>.
visibility=Redzamība visibility=Redzamība
visiblity_helper=This repository is <span class="ui red text">Private</span> visiblity_helper=Šis repozitorijs ir <span class="ui red text">privāts</span>
visiblity_fork_helper=(Change of this value will affect all forks) visiblity_helper_forced=Lapas administrators ir noteicis, ka visiem repozitorijiem ir jābūt <span class="ui red text">privātiem</span>
visiblity_fork_helper=(Šīs vērtības maiņa ietekmēs arī visus atdalītos repozitorijus)
fork_repo=Atdalīt repozitoriju fork_repo=Atdalīt repozitoriju
fork_from=Atdalīt no fork_from=Atdalīt no
fork_visiblity_helper=Atdalītam repozitorijam nav iespējams nomainīt tā redzamību fork_visiblity_helper=Atdalītam repozitorijam nav iespējams nomainīt tā redzamību
repo_desc=Apraksts repo_desc=Apraksts
repo_lang=Valoda repo_lang=Valoda
repo_lang_helper=Select .gitignore files repo_lang_helper=Izvēlieties .gitignore failus
license=Licence license=Licence
license_helper=Izvēlieties licences failu license_helper=Izvēlieties licences failu
readme=Readme readme=LasiMani
readme_helper=Select a readme template readme_helper=Izvēlieties faila LasiMani sagatavi
auto_init=Initialize this repository with selected files and template auto_init=Inicializēt šo repozitoriju ar izvēlētajiem failiem un sagatavi
create_repo=Izveidot repozitoriju create_repo=Izveidot repozitoriju
default_branch=Noklusējuma atzars default_branch=Noklusējuma atzars
mirror_interval=Spoguļošanas intervāls (stundās) mirror_interval=Spoguļošanas intervāls (stundās)
form.name_reserved=Repository name '%s' is reserved. form.name_reserved=Repozitorija nosaukums '%s' ir rezervēts.
form.name_pattern_not_allowed=Repository name pattern '%s' is not allowed. form.name_pattern_not_allowed=Repozitorija nosaukums '%s' nav atļauts.
need_auth=Nepieciešama autorizācija need_auth=Nepieciešama autorizācija
migrate_type=Migrācijas veids migrate_type=Migrācijas veids
migrate_type_helper=This repository will be a <span class="text blue">mirror</span> migrate_type_helper=Šis repozitorijs būs <span class="text blue">spogulis</span>
migrate_repo=Migrēt repozitoriju migrate_repo=Migrēt repozitoriju
migrate.clone_address=Clone Address migrate.clone_address=Klonēšanas adrese
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path. migrate.clone_address_desc=Tas var būt HTTP/HTTPS/GIT URL vai ceļš uz lokālā servera.
migrate.invalid_local_path=Invalid local path, it does not exist or not a directory. migrate.permission_denied=Jums nav tiesību importēt lokālu repozitoriju.
migrate.invalid_local_path=Nekorents lokālais ceļš, tas neeksistē vai nav direktorijs.
forked_from=forked from forked_from=atdalīts no
fork_from_self=You cannot fork repository you already owned! fork_from_self=Nav iespējams atdalīt repozitoriju, kuram esat īpašnieks!
copy_link=Kopēt copy_link=Kopēt
copy_link_success=Copied! copy_link_success=Nokopēts!
copy_link_error=Press ⌘-C or Ctrl-C to copy copy_link_error=Nospiediet ⌘-C vai Ctrl-C, lai nokopētu
click_to_copy=Kopēt uz starpliktuvi click_to_copy=Kopēt uz starpliktuvi
copied=Kopēšana notikusi veiksmīgi copied=Kopēšana notikusi veiksmīgi
clone_helper=Nepieciešama palīdzība kā veikt klonēšana? Apmeklējiet <a target="_blank" href="%s">Palīdzība</a> lapu! clone_helper=Nepieciešama palīdzība kā veikt klonēšana? Apmeklējiet <a target="_blank" href="%s">Palīdzība</a> lapu!
@ -380,7 +382,7 @@ quick_guide=Īsa pamācība
clone_this_repo=Klonēt šo repozitoriju clone_this_repo=Klonēt šo repozitoriju
create_new_repo_command=Izveidot jaunu repozitoriju komandrindā create_new_repo_command=Izveidot jaunu repozitoriju komandrindā
push_exist_repo=Nosūtīt izmaiņas no komandrindas eksistējošam repozitorijam push_exist_repo=Nosūtīt izmaiņas no komandrindas eksistējošam repozitorijam
repo_is_empty=This repository is empty, please come back later! repo_is_empty=Šis repozitorijs ir tukšs, apskatiet atkal vēlāk!
branch=Atzars branch=Atzars
@ -389,15 +391,15 @@ branch_and_tags=Atzari un tagi
branches=Atzari branches=Atzari
tags=Tagi tags=Tagi
issues=Problēmas issues=Problēmas
pulls=Pull Requests pulls=Izmaiņu pieprasījumi
labels=Labels labels=Etiķetes
milestones=Milestones milestones=Atskaites punkti
commits=Revīzijas commits=Revīzijas
releases=Laidieni releases=Laidieni
file_raw=Neapstrādāts file_raw=Neapstrādāts
file_history=Vēsture file_history=Vēsture
file_view_raw=Rādīt neapstrādātu file_view_raw=Rādīt neapstrādātu
file_permalink=Permalink file_permalink=Patstāvīgā saite
commits.commits=Revīzijas commits.commits=Revīzijas
commits.search=Meklēt revīzijas commits.search=Meklēt revīzijas
@ -408,124 +410,126 @@ commits.date=Datums
commits.older=Vecāki commits.older=Vecāki
commits.newer=Jaunāki commits.newer=Jaunāki
issues.new=New Issue issues.new=Jauna problēma
issues.new.labels=Labels issues.new.labels=Etiķetes
issues.new.no_label=No Label issues.new.no_label=Nav etiķešu
issues.new.clear_labels=Clear labels issues.new.clear_labels=Noņemt etiķetes
issues.new.milestone=Milestone issues.new.milestone=Atskaites punkts
issues.new.no_milestone=No Milestone issues.new.no_milestone=Nav atskaites punktu
issues.new.clear_milestone=Clear milestone issues.new.clear_milestone=Notīrīt atskaites punktus
issues.new.open_milestone=Open Milestones issues.new.open_milestone=Atvērtie atskaites punktus
issues.new.closed_milestone=Closed Milestones issues.new.closed_milestone=Aizvērtie atskaites punkti
issues.new.assignee=Assignee issues.new.assignee=Atbildīgais
issues.new.clear_assignee=Clear assignee issues.new.clear_assignee=Noņemt atbildīgo
issues.new.no_assignee=No assignee issues.new.no_assignee=Nav atbildīgā
issues.create=Create Issue issues.create=Pieteikt problēmu
issues.new_label=New Label issues.new_label=Jauna etiķete
issues.new_label_placeholder=Label name... issues.new_label_placeholder=Etiķetes nosaukums...
issues.create_label=Create Label issues.create_label=Izveidot etiķeti
issues.open_tab=%d Open issues.open_tab=%d atvērti
issues.close_tab=%d Closed issues.close_tab=%d aizvērti
issues.filter_label=Label issues.filter_label=Etiķete
issues.filter_label_no_select=No selected label issues.filter_label_no_select=Nav atzīmēta etiķete
issues.filter_milestone=Milestone issues.filter_milestone=Atskaites punkts
issues.filter_milestone_no_select=No selected milestone issues.filter_milestone_no_select=Nav atzīmēts atskaites punkts
issues.filter_assignee=Assignee issues.filter_assignee=Atbildīgais
issues.filter_assginee_no_select=No selected Assignee issues.filter_assginee_no_select=Nav atzīmēts atbildīgais
issues.filter_type=Type issues.filter_type=Veids
issues.filter_type.all_issues=All issues issues.filter_type.all_issues=Visas problēmas
issues.filter_type.assigned_to_you=Assigned to you issues.filter_type.assigned_to_you=Piešķirtās Jums
issues.filter_type.created_by_you=Created by you issues.filter_type.created_by_you=Jūsu izveidotās
issues.filter_type.mentioning_you=Mentioning you issues.filter_type.mentioning_you=Esat pieminēts
issues.filter_sort=Kārtot issues.filter_sort=Kārtot
issues.filter_sort.latest=Jaunākie issues.filter_sort.latest=Jaunākie
issues.filter_sort.oldest=Vecakie issues.filter_sort.oldest=Vecakie
issues.filter_sort.recentupdate=Recently updated issues.filter_sort.recentupdate=Nesen atjaunotās
issues.filter_sort.leastupdate=Least recently updated issues.filter_sort.leastupdate=Vissenāk atjaunotās
issues.filter_sort.mostcomment=Most commented issues.filter_sort.mostcomment=Visvairāk komentētās
issues.filter_sort.leastcomment=Least commented issues.filter_sort.leastcomment=Vismazāk komentētās
issues.opened_by=opened %[1]s by <a href="%[2]s">%[3]s</a> issues.opened_by=<a href="%[2]s">%[3]s</a> atvēra %[1]s
issues.opened_by_fake=opened %[1]s by %[2]s issues.opened_by_fake=%[2]s atvēra %[1]s
issues.previous=Previous issues.previous=Iepriekšējā
issues.next=Next issues.next=Nākamā
issues.open_title=Atvērta issues.open_title=Atvērta
issues.closed_title=Slēgta issues.closed_title=Slēgta
issues.num_comments=%d komentāri issues.num_comments=%d komentāri
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at=`komentēja <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=There is no content yet. issues.no_content=Vēl nav satura.
issues.close_issue=Aizvērt issues.close_issue=Aizvērt
issues.close_comment_issue=Close and comment issues.close_comment_issue=Komentēt un aizvērt
issues.reopen_issue=Reopen issues.reopen_issue=Atvērt atkārtoti
issues.reopen_comment_issue=Reopen and comment issues.reopen_comment_issue=Komentēt un atvērt atkārtoti
issues.create_comment=Comment issues.create_comment=Komentēt
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`aizvērts <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`atvērts atkārtoti <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at=`pieminēja šo problēmu revīzijā <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.poster=Poster issues.poster=Autors
issues.admin=Admin issues.admin=Administrators
issues.owner=Owner issues.owner=Īpašnieks
issues.sign_up_for_free=Sign up for free issues.sign_up_for_free=Pievienojieties
issues.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a> issues.sign_in_require_desc=, lai piedalītos diskusijā. Jau ir konts? <a href="%s">Pierakstieties, lai komentētu</a>
issues.edit=Labot issues.edit=Labot
issues.cancel=Atcelt issues.cancel=Atcelt
issues.save=Saglabāt issues.save=Saglabāt
issues.label_title=Label name issues.label_title=Etiķetes nosaukums
issues.label_color=Label color issues.label_color=Etiķetes krāsa
issues.label_count=%d labels issues.label_count=%d etiķetes
issues.label_open_issues=%d open issues issues.label_open_issues=%d atvērtas problēmas
issues.label_edit=Edit issues.label_edit=Labot
issues.label_delete=Delete issues.label_delete=Dzēst
issues.label_modify=Label Modification issues.label_modify=Etiķetes labošana
issues.label_deletion=Label Deletion issues.label_deletion=Etiķetes dzēšana
issues.label_deletion_desc=Delete this label will remove its information in all related issues. Do you want to continue? issues.label_deletion_desc=Dzēšot šo etiķeti, tā tiks noņemta no visām saistītajām problēmām. Vai vēlaties turpināt?
issues.label_deletion_success=Label has been deleted successfully! issues.label_deletion_success=Etiķete tika veiksmīgi izdzēsta!
pulls.compare_changes=Compare Changes pulls.compare_changes=Salīdzināt izmaiņas
pulls.compare_changes_desc=Compare two branches and make a pull request for changes. pulls.compare_changes_desc=Salīdzināt divus atzarus un izveidot izmaiņu pieprasījumu.
pulls.compare_base=base pulls.compare_base=pamata
pulls.compare_compare=compare pulls.compare_compare=salīdzināmais
pulls.filter_branch=Filter branch pulls.filter_branch=Filtrēt atzarus
pulls.no_results=No results found. pulls.no_results=Nekas netika atrasts.
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even. pulls.nothing_to_compare=Nav ko salīdzināt, jo bāzes un salīdzināmie atzari ir vienādi.
pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>` pulls.has_pull_request=`Jau eksistē izmaiņu pieprasījums starp šiem diviem atzariem: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
pulls.create=Create Pull Request pulls.create=Izveidot izmaiņu pieprasījumu
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> pulls.title_desc=vēlas sapludināt %[1]d revīzijas no <code>%[2]s</code> uz <code>%[3]s</code>
pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s pulls.merged_title_desc=sapludināja %[1]d revīzijas no <code>%[2]s</code> uz <code>%[3]s</code> %[4]s
pulls.tab_conversation=Conversation pulls.tab_conversation=Saruna
pulls.tab_commits=Commits pulls.tab_commits=Revīzijas
pulls.tab_files=Files changed pulls.tab_files=Izmainītie faili
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation. pulls.reopen_to_merge=Lūdzu, atkārtoti atveriet šo izmaiņu pieprasījumu, lai veiktu sapludināšanu.
pulls.merged=Merged pulls.merged=Sapludināts
pulls.has_merged=This pull request has been merged successfully! pulls.has_merged=Šo izmaiņu pieprasījums tika veiksmīgi sapludināts!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information. pulls.data_broken=Nepieejami izmaiņu pieprasījuma dati, jo dzēsta informācija no atdalītā repozitorija.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request. pulls.is_checking=Notiek konfliktu pārbaude, mirkli uzgaidiet un atjaunojiet lapu.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits. pulls.can_auto_merge_desc=Ir iespējams veikt automātisko sapludināšanas darbību šim izmaiņu pieprasījumam.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it. pulls.cannot_auto_merge_desc=Nav iespējams veikt automātisko sapludināšanas darbību, jo starp revīzijām ir konflikti.
pulls.merge_pull_request=Merge Pull Request pulls.cannot_auto_merge_helper=Lūdzu, izmantojiet komandrindas rīku, lai to atrisinātu.
pulls.merge_pull_request=Izmaiņu pieprasījuma sapludināšana
milestones.new=New Milestone pulls.open_unmerged_pull_exists=`Jūs nevarat veikt atkārtotas atvēršanas darbību, jo jau eksistē izmaiņu pieprasījums (#%d) no šī repozitorija ar tādu pašu sapludināšanas informāciju un gaida sapludināšanu.`
milestones.open_tab=%d Open
milestones.close_tab=%d Closed milestones.new=Jauns atskaites punkts
milestones.closed=Closed %s milestones.open_tab=%d atvērti
milestones.no_due_date=No due date milestones.close_tab=%d aizvērti
milestones.open=Open milestones.closed=Aizvērts %s
milestones.no_due_date=Bez termiņa
milestones.open=Atvērt
milestones.close=Aizvērt milestones.close=Aizvērt
milestones.new_subheader=Create milestones to organize your issues. milestones.new_subheader=Izveidojiet atskaites punktus, lai organizētu problēmas.
milestones.create=Create Milestone milestones.create=Izveidot atskaites punktu
milestones.title=Virsraksts milestones.title=Virsraksts
milestones.desc=Apraksts milestones.desc=Apraksts
milestones.due_date=Due Date (optional) milestones.due_date=Termiņš (neobligāts)
milestones.clear=Clear milestones.clear=Notīrīt
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'. milestones.invalid_due_date_format=Termiņa datuma formāts ir nekorekts, jābūt formātā 'gggg-mm-dd'.
milestones.create_success=Milestone '%s' has been created successfully! milestones.create_success=Atskaites punkts '%s' tika veiksmīgi izveidots!
milestones.edit=Edit Milestone milestones.edit=Labot atskaites punktu
milestones.edit_subheader=Use better description for milestones so people won't be confused. milestones.edit_subheader=Izmantojiet pēc iespējas labāku aprakstu atskaites punktiem, lai citiem tas būtu saprotamāks.
milestones.cancel=Atcelt milestones.cancel=Atcelt
milestones.modify=Modify Milestone milestones.modify=Mainīt atskaites punktu
milestones.edit_success=Changes of milestone '%s' has been saved successfully! milestones.edit_success=Izmaiņas atskaites punktā '%s' tika veiksmīgi saglabātas!
milestones.deletion=Milestone Deletion milestones.deletion=Atskaites punkta dzēšana
milestones.deletion_desc=Delete this milestone will remove its information in all related issues. Do you want to continue? milestones.deletion_desc=Dzēšot šo atskaites punktu tiks noņemta arī saistītā informācija no problēmu ziņojumiem. Vai vēlaties turpināt?
milestones.deletion_success=Milestone has been deleted successfully! milestones.deletion_success=Atskaites punkts tika veiksmīgi izdzēsts!
settings=Iestatījumi settings=Iestatījumi
settings.options=Opcijas settings.options=Opcijas
@ -536,20 +540,20 @@ settings.basic_settings=Pamatiestatījumi
settings.danger_zone=Bīstamā zona settings.danger_zone=Bīstamā zona
settings.site=Oficiālā mājas lapa settings.site=Oficiālā mājas lapa
settings.update_settings=Mainīt iestatījumus settings.update_settings=Mainīt iestatījumus
settings.change_reponame_prompt=This change will affect how links relate to the repository. settings.change_reponame_prompt=Šī izmaiņa ietekmēs saites, kas ir saistītas ar šo repozitoriju.
settings.transfer=Mainīt īpašnieku settings.transfer=Mainīt īpašnieku
settings.transfer_desc=Mainīt šī repozitorija īpašnieku uz citu lietotāju vai organizāciju, kurai Jums ir administratora tiesībs. settings.transfer_desc=Mainīt šī repozitorija īpašnieku uz citu lietotāju vai organizāciju, kurai Jums ir administratora tiesībs.
settings.new_owner_has_same_repo=Jaunajam īpašniekam jau ir repozitorijs ar šādu nosaukumu. settings.new_owner_has_same_repo=Jaunajam īpašniekam jau ir repozitorijs ar šādu nosaukumu.
settings.delete=Dzēst šo repozitoriju settings.delete=Dzēst šo repozitoriju
settings.delete_desc=Dzēšot repozitoriju, tā datus vairs nebūs iespējams atgūt. Pirms dzēšanas pārliecinieites vai patiešām vēlaties to darīt. settings.delete_desc=Dzēšot repozitoriju, tā datus vairs nebūs iespējams atgūt. Pirms dzēšanas pārliecinieites vai patiešām vēlaties to darīt.
settings.transfer_notices_1=- You will lose access if new owner is a individual user. settings.transfer_notices_1=- Jūs pazaudēsiet piekļuvi, ja jaunais īpašnieks ir lietotājs.
settings.transfer_notices_2=- You will conserve access if new owner is an organization and if you're one of the owners. settings.transfer_notices_2=- Jūs saglabāsiet piekļuvi, ja jaunais īpašnieks ir organizācija un Jūs esat viens no tās īpašniekiem.
settings.transfer_form_title=Please enter following information to confirm your operation: settings.transfer_form_title=Lūdzu, ievadiet sekojošu informāciju, lai apstiprinātu šo darbību:
settings.delete_notices_1=- This operation <strong>CANNOT</strong> be undone. settings.delete_notices_1=- Šī darbība ir <strong>NEATGRIEZENISKA</strong>.
settings.delete_notices_2=- This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators. settings.delete_notices_2=- Šī darbība neatgriezeniski izdzēsīs visus šī repozitorija datus, tai skaitā Git datus, problēmu ziņojumus, komentārus un definētās piekļuves tiesības.
settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion. settings.delete_notices_fork_1=- Ja repozitorijs ir publisks, visi atdalītie repozitoriji kļūs neatkarīgi.
settings.delete_notices_fork_2=- If this repository is private, all forks will be removed at the same time. settings.delete_notices_fork_2=- Ja repozitorijs ir privāts, tiks dzēsti arī visi atdalītie repozitoriji.
settings.delete_notices_fork_3=- If you want to keep all forks after deletion, please change visibility of this repository to public first. settings.delete_notices_fork_3=- Ja vēlaties saglabāt atdalīts repozitorijus pēc dzēšanas, sākumā nomainiet repozitorija redzamību uz publisku.
settings.update_settings_success=Repozitorija opcijas ir veiksmīgi saglabātas. settings.update_settings_success=Repozitorija opcijas ir veiksmīgi saglabātas.
settings.transfer_owner=Jaunais īpašnieks settings.transfer_owner=Jaunais īpašnieks
settings.make_transfer=Mainīt settings.make_transfer=Mainīt
@ -561,14 +565,14 @@ settings.remove_collaborator_success=Līdzstrādnieks tika noņemts.
settings.user_is_org_member=Lietotājs ir organizācijas biedrs, kas nevar tikt pievienots kā līdzstrādnieks. settings.user_is_org_member=Lietotājs ir organizācijas biedrs, kas nevar tikt pievienots kā līdzstrādnieks.
settings.add_webhook=Pievienot tīmekļa āķi settings.add_webhook=Pievienot tīmekļa āķi
settings.hooks_desc=Tīmekļa āķi ļauj paziņot ārējiem servisiem par noteiktiem notikomiem, kas notiek Git servisā. Kad iestāsies kāds notikums, katram ārējā servisa URL tiks nosūtīts POST pieprasījums. Lai uzzinātu sīkāk skatieties <a target="_blank" href="%s">Tīmekļa āķu rokasgrāmatā</a>. settings.hooks_desc=Tīmekļa āķi ļauj paziņot ārējiem servisiem par noteiktiem notikomiem, kas notiek Git servisā. Kad iestāsies kāds notikums, katram ārējā servisa URL tiks nosūtīts POST pieprasījums. Lai uzzinātu sīkāk skatieties <a target="_blank" href="%s">Tīmekļa āķu rokasgrāmatā</a>.
settings.webhook_deletion=Delete Webhook settings.webhook_deletion=Dzēst tīmekļa āķi
settings.webhook_deletion_desc=Delete this webhook will remove its information and all delivery history. Do you want to continue? settings.webhook_deletion_desc=Dzēšot tīmekļa āķi tiks dzēsta visa ar to saistītā informācija un izpildes vēsture. Vai vēlaties turpināt?
settings.webhook_deletion_success=Webhook has been deleted successfully! settings.webhook_deletion_success=Tīmekļa āķis tika veiksmīgi izdzēsts!
settings.webhook.request=Request settings.webhook.request=Pieprasījums
settings.webhook.response=Response settings.webhook.response=Atbilde
settings.webhook.headers=Headers settings.webhook.headers=Galvenes
settings.webhook.payload=Payload settings.webhook.payload=Derīgā krava
settings.webhook.body=Body settings.webhook.body=Saturs
settings.githooks_desc=Git āķus apstrādā pats Git. Jūs varat labot atbalsīto āku failus sarakstā zemāk, lai veiktu pielāgotas darbības. settings.githooks_desc=Git āķus apstrādā pats Git. Jūs varat labot atbalsīto āku failus sarakstā zemāk, lai veiktu pielāgotas darbības.
settings.githook_edit_desc=Ja āķis nav aktīvs, tiks attēlots piemērs kā to izmantot. Atstājot āķa saturu tukšu, tas tiks atspējots. settings.githook_edit_desc=Ja āķis nav aktīvs, tiks attēlots piemērs kā to izmantot. Atstājot āķa saturu tukšu, tas tiks atspējots.
settings.githook_name=Āķa nosaukums settings.githook_name=Āķa nosaukums
@ -578,17 +582,17 @@ settings.add_webhook_desc=Uz norādīto URL tiks nosūtīts <code>POST</code> pi
settings.payload_url=Vērtuma URL settings.payload_url=Vērtuma URL
settings.content_type=Satura tips settings.content_type=Satura tips
settings.secret=Noslēpums settings.secret=Noslēpums
settings.slack_username=Username settings.slack_username=Lietotājvārds
settings.slack_icon_url=Icon URL settings.slack_icon_url=Ikonas URL
settings.slack_color=Color settings.slack_color=Krāsa
settings.event_desc=Kādu notikumu rezultātā tiktu izsaukts tīmekļā āķis? settings.event_desc=Kādu notikumu rezultātā tiktu izsaukts tīmekļā āķis?
settings.event_push_only=Tikai izmaiņu nosūtīšanas notikumiem. settings.event_push_only=Tikai izmaiņu nosūtīšanas notikumiem.
settings.event_send_everything=I need <strong>everything</strong>. settings.event_send_everything=Vēlos saņemt <strong>visu</strong>.
settings.event_choose=Let me choose what I need. settings.event_choose=Atzīmēt, ko vēlos saņemt.
settings.event_create=Create settings.event_create=Izveidot
settings.event_create_desc=Branch, or tag created settings.event_create_desc=Atzara vai taga izveidošana
settings.event_push=Push settings.event_push=Izmaiņu nosūtīšana
settings.event_push_desc=Git push to a repository settings.event_push_desc=Git izmaiņu nosūtīšana uz repozitoriju
settings.active=Aktīvs settings.active=Aktīvs
settings.active_helper=Tiks nosūtīti notikuma dati, kad nostrādās šis āķis. settings.active_helper=Tiks nosūtīti notikuma dati, kad nostrādās šis āķis.
settings.add_hook_success=Jauns tīmekļa āķis tika veiksmīgi pievienots. settings.add_hook_success=Jauns tīmekļa āķis tika veiksmīgi pievienots.
@ -602,16 +606,16 @@ settings.slack_token=Talons
settings.slack_domain=Domēns settings.slack_domain=Domēns
settings.slack_channel=Kanāls settings.slack_channel=Kanāls
settings.deploy_keys=Izvietot atslēgas settings.deploy_keys=Izvietot atslēgas
settings.add_deploy_key=Add Deploy Key settings.add_deploy_key=Pievienot izvietošanas atslēgu
settings.no_deploy_keys=You haven't added any deploy key. settings.no_deploy_keys=Nav pievienota neviena izvietošanas atslēga.
settings.title=Virsraksts settings.title=Virsraksts
settings.deploy_key_content=Saturs settings.deploy_key_content=Saturs
settings.key_been_used=Deploy key content has been used. settings.key_been_used=Šāda izvietošanas atslēga jau eksistē.
settings.key_name_used=Deploy key with same name has already existed. settings.key_name_used=Izvietošanas atslēga ar šādu nosaukumu jau eksistē.
settings.add_key_success=New deploy key '%s' has been added successfully! settings.add_key_success=Izvietošanas atslēga '%s' tik veiksmīgi pievienota!
settings.deploy_key_deletion=Delete Deploy Key settings.deploy_key_deletion=Dzēst izvietošanas atslēgu
settings.deploy_key_deletion_desc=Delete this deploy key will remove all related accesses for this repository. Do you want to continue? settings.deploy_key_deletion_desc=Dzēšot šo izvietošanas atslēgu tiks noņemta arī ar to saistītā piekļuve šim repozitorijam. Vai vēlaties turpināt?
settings.deploy_key_deletion_success=Deploy key has been deleted successfully! settings.deploy_key_deletion_success=Izvietošanas atslēga tika veiksmīgi izdzēsta!
diff.browse_source=Pārlūkot izejas kodu diff.browse_source=Pārlūkot izejas kodu
diff.parent=vecāks diff.parent=vecāks
@ -648,6 +652,7 @@ release.tag_name_already_exist=Laidiens ar šādu taga nosaukumu jau eksistē.
[org] [org]
org_name_holder=Organizācijas nosaukums org_name_holder=Organizācijas nosaukums
org_full_name_holder=Organizācijas pilnais nosaukums
org_name_helper=Labi organizāciju nosaukumi ir īsi un tādi, kurus viegli atcerēties. org_name_helper=Labi organizāciju nosaukumi ir īsi un tādi, kurus viegli atcerēties.
create_org=Izveidot organizāciju create_org=Izveidot organizāciju
repo_updated=Atjaunināts repo_updated=Atjaunināts
@ -664,8 +669,8 @@ team_name_helper=Šo nosaukumu varēs izmantot, lai pieminētu komandu sarunās.
team_desc_helper=Komandas apraksts team_desc_helper=Komandas apraksts
team_permission_desc=Kādām tiesībām šai komandai būtu jābūt? team_permission_desc=Kādām tiesībām šai komandai būtu jābūt?
form.name_reserved=Organization name '%s' is reserved. form.name_reserved=Organizācijas nosaukums '%s' ir rezervēts.
form.name_pattern_not_allowed=Organization name pattern '%s' is not allowed. form.name_pattern_not_allowed=Organizācijas nosaukums '%s' nav atļauts.
settings=Iestatījumi settings=Iestatījumi
settings.options=Opcijas settings.options=Opcijas
@ -674,8 +679,8 @@ settings.website=Mājas lapa
settings.location=Atrašanās vieta settings.location=Atrašanās vieta
settings.update_settings=Mainīt iestatījumus settings.update_settings=Mainīt iestatījumus
settings.update_setting_success=Organizācijas iestatījumi tika veiksmīgi saglabāti. settings.update_setting_success=Organizācijas iestatījumi tika veiksmīgi saglabāti.
settings.change_orgname_prompt=This change will affect how links relate to the organization. settings.change_orgname_prompt=Šī izmaiņa ietekmēs saites, kas ir saistītas ar šo organizāciju.
settings.update_avatar_success=Organization avatar setting has been updated successfully. settings.update_avatar_success=Organizācijas avatara iestatījumi tika veiksmīgi saglabāti.
settings.delete=Dzēst organizāciju settings.delete=Dzēst organizāciju
settings.delete_account=Dzēst šo organizāciju settings.delete_account=Dzēst šo organizāciju
settings.delete_prompt=Šī darbība pilnībā dzēsīs šo organizāciju, kā arī tā ir <strong>NEATGRIEZENISKA</strong>! settings.delete_prompt=Šī darbība pilnībā dzēsīs šo organizāciju, kā arī tā ir <strong>NEATGRIEZENISKA</strong>!
@ -731,9 +736,9 @@ authentication=Autentifikācijas
config=Konfigurācija config=Konfigurācija
notices=Sistēmas paziņojumi notices=Sistēmas paziņojumi
monitor=Uzraudzība monitor=Uzraudzība
first_page=First first_page=Pirmā
last_page=Last last_page=Pēdējā
total=Total: %d total=Kopā: %d
dashboard.statistic=Statistika dashboard.statistic=Statistika
dashboard.operations=Darbības dashboard.operations=Darbības
@ -792,23 +797,24 @@ users.activated=Aktivizēts
users.admin=Administrators users.admin=Administrators
users.repos=Repozitoriji users.repos=Repozitoriji
users.created=Izveidots users.created=Izveidots
users.send_register_notify=Send Registration Notification To User users.send_register_notify=Nosūtīt lietotājam reģistrācijas paziņojumu
users.new_success=New account '%s' has been created successfully. users.new_success=Jauns konts '%s' tika veiksmīgi izveidots.
users.edit=Labot users.edit=Labot
users.auth_source=Authentication Source users.auth_source=Autentificēšanas avots
users.local=Iebūvētā users.local=Iebūvētā
users.auth_login_name=Authentication Login Name users.auth_login_name=Autentifikācijas pieteikšanās vārds
users.password_helper=Leave it empty to remain unchanged. users.password_helper=Atstājiet tukšu, ja nevēlaties mainīt.
users.update_profile_success=Konta profils tika veiksmīgi saglabāts. users.update_profile_success=Konta profils tika veiksmīgi saglabāts.
users.edit_account=Labot kontu users.edit_account=Labot kontu
users.is_activated=Konts ir aktivizēts users.is_activated=Konts ir aktivizēts
users.is_admin=Šim kontam ir administratora piekļuves tiesības users.is_admin=Šim kontam ir administratora piekļuves tiesības
users.allow_git_hook=Šim kontam ir tiesības pievienot/labot Git āķus users.allow_git_hook=Šim kontam ir tiesības pievienot/labot Git āķus
users.allow_import_local=Šim kontam ir tiesības importēt lokālus repozitorijus
users.update_profile=Mainīt konta profilu users.update_profile=Mainīt konta profilu
users.delete_account=Dzēst šo kontu users.delete_account=Dzēst šo kontu
users.still_own_repo=Šis konts ir vismaz viena repozitorija īpašnieks, tos sākumā ir nepieciešams izdzēst vai nomainīt to īpašnieku. users.still_own_repo=Šis konts ir vismaz viena repozitorija īpašnieks, tos sākumā ir nepieciešams izdzēst vai nomainīt to īpašnieku.
users.still_has_org=Šis konts ir vismaz vienas organizācijas biedrs, sākumā nepieciešams pamest vai izdzēst šo organizāciju. users.still_has_org=Šis konts ir vismaz vienas organizācijas biedrs, sākumā nepieciešams pamest vai izdzēst šo organizāciju.
users.deletion_success=Account has been deleted successfully! users.deletion_success=Konts tika veiksmīgi izdzēsts!
orgs.org_manage_panel=Organizāciju pārvaldības panelis orgs.org_manage_panel=Organizāciju pārvaldības panelis
orgs.name=Nosaukums orgs.name=Nosaukums
@ -823,47 +829,47 @@ repos.watches=Vērošana
repos.stars=Atzīmētās zvaigznītes repos.stars=Atzīmētās zvaigznītes
repos.issues=Problēmas repos.issues=Problēmas
auths.auth_manage_panel=Authentication Manage Panel auths.auth_manage_panel=Autentifikācijas pārvaldības panelis
auths.new=Add New Source auths.new=Pievienot jaunu avotu
auths.name=Nosaukums auths.name=Nosaukums
auths.type=Veids auths.type=Veids
auths.enabled=Iespējota auths.enabled=Iespējota
auths.updated=Atjaunināta auths.updated=Atjaunināta
auths.auth_type=Authentication Type auths.auth_type=Autentifikācijas tips
auths.auth_name=Authentication Name auths.auth_name=Autentifikācijas nosaukums
auths.domain=Domēns auths.domain=Domēns
auths.host=Resursdators auths.host=Resursdators
auths.port=Ports auths.port=Ports
auths.bind_dn=Bind DN auths.bind_dn=Saistīšanas DN
auths.bind_password=Bind Password auths.bind_password=Saistīšanas parole
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account. auths.bind_password_helper=Brīdinājums: Šī parole tiks saglabāta nešifrētā veidā. Neizmantojiet kontu ar augstām privilēģijām.
auths.user_base=User Search Base auths.user_base=Lietotāja pamatnosacījumi
auths.user_dn=User DN auths.user_dn=Lietotāja DN
auths.attribute_name=First name attribute auths.attribute_name=Vārda atribūts
auths.attribute_surname=Surname attribute auths.attribute_surname=Uzvārda atribūts
auths.attribute_mail=E-mail attribute auths.attribute_mail=E-pasta atribūts
auths.filter=User Filter auths.filter=Lietotāju filts
auths.admin_filter=Admin Filter auths.admin_filter=Administratoru filtrs
auths.ms_ad_sa=MS Ad SA auths.ms_ad_sa=MS Ad SA
auths.smtp_auth=SMTP Authentication Type auths.smtp_auth=SMTP autentifikācijas tips
auths.smtphost=SMTP resursdators auths.smtphost=SMTP resursdators
auths.smtpport=SMTP ports auths.smtpport=SMTP ports
auths.allowed_domains=Allowed Domains auths.allowed_domains=Atļautie domēni
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','. auths.allowed_domains_helper=Atstājiet tukšu, ja nevēlaties ierobežot domēnu vārdus. Domēna vārdus nepieciešams atdalīt ar komatu ','.
auths.enable_tls=Iespējot TLS šifrēšanu auths.enable_tls=Iespējot TLS šifrēšanu
auths.skip_tls_verify=Skip TLS Verify auths.skip_tls_verify=Izlaist TLS verifikāciju
auths.pam_service_name=PAM Service Name auths.pam_service_name=PAM servisa nosaukums
auths.enable_auto_register=Iespējot automātisko reģistrāciju auths.enable_auto_register=Iespējot automātisko reģistrāciju
auths.tips=Padomi auths.tips=Padomi
auths.edit=Edit Authentication Setting auths.edit=Labot autentifikācijas iestatījumus
auths.activated=Autentifikācija ir aktivizēta auths.activated=Autentifikācija ir aktivizēta
auths.new_success=New authentication '%s' has been added successfully. auths.new_success=Jauna autentifikācija '%s' tika veiksmīgi pievienota.
auths.update_success=Authentication setting has been updated successfully. auths.update_success=Autentifikācijas iestatījumi tika veiksmīgi saglabāti.
auths.update=Update Authentication Setting auths.update=Mainīt autentifikācijas iestatījumus
auths.delete=Delete This Authentication auths.delete=Dzēst šo autentifikāciju
auths.delete_auth_title=Authentication Deletion auths.delete_auth_title=Autentifikācijas dzēšana
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue? auths.delete_auth_desc=Šī autentifikācija tiks dzēsta, vai vēlaties turpināt?
auths.deletion_success=Authentication has been deleted successfully! auths.deletion_success=Autentifikācija tika veiksmīgi izdzēsta!
config.server_config=Servera konfigurācija config.server_config=Servera konfigurācija
config.app_name=Lietotnes nosaukums config.app_name=Lietotnes nosaukums
@ -887,7 +893,7 @@ config.db_user=Lietotājs
config.db_ssl_mode=SSL režīms config.db_ssl_mode=SSL režīms
config.db_ssl_mode_helper=(tikai PostgreSQL datu bāzei) config.db_ssl_mode_helper=(tikai PostgreSQL datu bāzei)
config.db_path=Ceļš config.db_path=Ceļš
config.db_path_helper=(for "sqlite3" and "tidb") config.db_path_helper=(priekš "sqlite3" and "tidb")
config.service_config=Pakalpojuma konfigurācija config.service_config=Pakalpojuma konfigurācija
config.register_email_confirm=Pieprasīt e-pasta apstiprināšanu config.register_email_confirm=Pieprasīt e-pasta apstiprināšanu
config.disable_register=Atspējot jaunu lietotāju reģistrāciju config.disable_register=Atspējot jaunu lietotāju reģistrāciju
@ -895,17 +901,17 @@ config.show_registration_button=Rādīt reģistrēšanās pogu
config.require_sign_in_view=Nepieciešama autorizācija config.require_sign_in_view=Nepieciešama autorizācija
config.enable_cache_avatar=Glabāt profila attēlus kešatmiņā config.enable_cache_avatar=Glabāt profila attēlus kešatmiņā
config.mail_notify=Pasta paziņojumi config.mail_notify=Pasta paziņojumi
config.disable_key_size_check=Disable Minimum Key Size Check config.disable_key_size_check=Atspējot atslēgas minimālā garuma pārbaudi
config.enable_captcha=Enable Captcha config.enable_captcha=Iespējot drošības kodu
config.active_code_lives=Aktīvā koda ilgums config.active_code_lives=Aktīvā koda ilgums
config.reset_password_code_lives=Paroles atiestatīšanas koda ilgums config.reset_password_code_lives=Paroles atiestatīšanas koda ilgums
config.webhook_config=Tīkla āķu konfigurācija config.webhook_config=Tīkla āķu konfigurācija
config.queue_length=Queue Length config.queue_length=Rindas garums
config.deliver_timeout=Piegādes noildze config.deliver_timeout=Piegādes noildze
config.skip_tls_verify=Izlaist TLS pārbaudi config.skip_tls_verify=Izlaist TLS pārbaudi
config.mailer_config=Sūtītāja konfigurācija config.mailer_config=Sūtītāja konfigurācija
config.mailer_enabled=Iespējots config.mailer_enabled=Iespējots
config.mailer_disable_helo=Disable HELO config.mailer_disable_helo=Atspējot HELO
config.mailer_name=Nosaukums config.mailer_name=Nosaukums
config.mailer_host=Resursdators config.mailer_host=Resursdators
config.mailer_user=Lietotājs config.mailer_user=Lietotājs
@ -950,12 +956,12 @@ notices.delete_success=Sistēmas paziņojums tika veiksmīgi izdzēsts.
[action] [action]
create_repo=izveidoja repozitoriju <a href="%s">%s</a> create_repo=izveidoja repozitoriju <a href="%s">%s</a>
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a> rename_repo=pārsauca repozitoriju no <code>%[1]s</code> uz <a href="%[2]s">%[3]s</a>
commit_repo=veica izmaiņu nosūtīšanu atzaram <a href="%s/src/%s">%[2]s</a> repozitorijā <a href="%[1]s">%[3]s</a> commit_repo=veica izmaiņu nosūtīšanu atzaram <a href="%s/src/%s">%[2]s</a> repozitorijā <a href="%[1]s">%[3]s</a>
create_issue=`reģistrēja problēmu <a href="%s/issues/%s">%s#%[2]s</a>` create_issue=`reģistrēja problēmu <a href="%s/issues/%s">%s#%[2]s</a>`
create_pull_request=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>` create_pull_request=`izveidoja izmaiņu pieprasījumu <a href="%s/pulls/%s">%s#%[2]s</a>`
comment_issue=`pievienoja komentāru problēmai <a href="%s/issues/%s">%s#%[2]s</a>` comment_issue=`pievienoja komentāru problēmai <a href="%s/issues/%s">%s#%[2]s</a>`
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>` merge_pull_request=`sapludināja izmaiņu pieprasījumu <a href="%s/pulls/%s">%s#%[2]s</a>`
transfer_repo=mainīja repozitorija <code>%s</code> īpašnieku uz <a href="%s">%s</a> transfer_repo=mainīja repozitorija <code>%s</code> īpašnieku uz <a href="%s">%s</a>
push_tag=pievienoja tagu <a href="%s/src/%s">%[2]s</a> repozitorijam <a href="%[1]s">%[3]s</a> push_tag=pievienoja tagu <a href="%s/src/%s">%[2]s</a> repozitorijam <a href="%[1]s">%[3]s</a>
compare_2_commits=Veikt salīdzināšanu starp šīm 2 revīzijām compare_2_commits=Veikt salīdzināšanu starp šīm 2 revīzijām
@ -982,8 +988,8 @@ raw_seconds=sekundes
raw_minutes=minūtes raw_minutes=minūtes
[dropzone] [dropzone]
default_message=Drop files here or click to upload. default_message=Ievelciet failus šeit vai noklikšķiniet, lai augšupielādētu.
invalid_input_type=You can't upload files of this type. invalid_input_type=Šādus failus nav iespējams augšupielādēt.
file_too_big=Faila izmērs ({{filesize}} MB) pārsniedz maksimālo atļauto izmēru ({{maxFilesize}} MB). file_too_big=Faila izmērs ({{filesize}} MB) pārsniedz maksimālo atļauto izmēru ({{maxFilesize}} MB).
remove_file=Noņemt failu remove_file=Noņemt failu

12
conf/locale/locale_nl-NL.ini

@ -111,7 +111,7 @@ admin_title=Instellingen beheerdersaccount
admin_name=Gebruikersnaam admin_name=Gebruikersnaam
admin_password=Wachtwoord admin_password=Wachtwoord
confirm_password=Verifieer wachtwoord confirm_password=Verifieer wachtwoord
admin_email=E-mailadres admin_email=Admin E-mail
install_gogs=Installeer Gogs install_gogs=Installeer Gogs
test_git_failed=Git test niet gelukt: 'git' commando %v test_git_failed=Git test niet gelukt: 'git' commando %v
sqlite3_not_available=Uw versie biedt geen ondersteuning voor SQLite3, download de officiële binaire versie van %s, niet de gobuild versie. sqlite3_not_available=Uw versie biedt geen ondersteuning voor SQLite3, download de officiële binaire versie van %s, niet de gobuild versie.
@ -148,7 +148,6 @@ forgot_password=Wachtwoord vergeten
forget_password=Wachtwoord vergeten? forget_password=Wachtwoord vergeten?
sign_up_now=Een account nodig? Meld u nu aan. sign_up_now=Een account nodig? Meld u nu aan.
confirmation_mail_sent_prompt=Een bevestigingsemail is gestuurd naar <b>%s</b>, Bevestig u aanvraag binnen %d uren om uw registratie te voltooien. confirmation_mail_sent_prompt=Een bevestigingsemail is gestuurd naar <b>%s</b>, Bevestig u aanvraag binnen %d uren om uw registratie te voltooien.
sign_in_to_account=Sign in to your account
active_your_account=Activeer uw account active_your_account=Activeer uw account
resent_limit_prompt=Sorry, u heeft te snel na elkaar een aanvraag gedaan voor een activatie mail. Wacht drie minuten voor uw volgende aanvraag. resent_limit_prompt=Sorry, u heeft te snel na elkaar een aanvraag gedaan voor een activatie mail. Wacht drie minuten voor uw volgende aanvraag.
has_unconfirmed_mail=Beste %s, u heeft een onbevestigd e-mailadres (<b>%s</b>). Als u nog geen bevestiging heeft ontvangen, of u een nieuwe aanvraag wilt doen, klik dan op de onderstaande knop. has_unconfirmed_mail=Beste %s, u heeft een onbevestigd e-mailadres (<b>%s</b>). Als u nog geen bevestiging heeft ontvangen, of u een nieuwe aanvraag wilt doen, klik dan op de onderstaande knop.
@ -192,6 +191,7 @@ min_size_error=moet minimaal %s karakters bevatten.
max_size_error=mag maximaal %s karakters bevatten. max_size_error=mag maximaal %s karakters bevatten.
email_error=is niet een valide e-mail adres. email_error=is niet een valide e-mail adres.
url_error=is niet een valide URL. url_error=is niet een valide URL.
include_error=` must contain substring '%s'.`
unknown_error=Onbekende fout: unknown_error=Onbekende fout:
captcha_incorrect=Captcha komt niet overeen. captcha_incorrect=Captcha komt niet overeen.
password_not_match=Wachtwoord en verificatie wachtwoord komen niet overeen. password_not_match=Wachtwoord en verificatie wachtwoord komen niet overeen.
@ -334,6 +334,7 @@ repo_name=Repositorie naam
repo_name_helper=Een goede repositorie naam is kort, memorabel en <strong>uniek</strong>. repo_name_helper=Een goede repositorie naam is kort, memorabel en <strong>uniek</strong>.
visibility=Zichtbaarheid visibility=Zichtbaarheid
visiblity_helper=Deze repositorie is <span class="ui red text">privaat</span> visiblity_helper=Deze repositorie is <span class="ui red text">privaat</span>
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
visiblity_fork_helper=(Verandering van deze waarde zal van invloed zijn op alle forks) visiblity_fork_helper=(Verandering van deze waarde zal van invloed zijn op alle forks)
fork_repo=Vork Repository fork_repo=Vork Repository
fork_from=Afsplitsing van fork_from=Afsplitsing van
@ -359,6 +360,7 @@ migrate_type_helper=Deze repositorie zal een <span class="text blue">mirror</spa
migrate_repo=Migreer repositorie migrate_repo=Migreer repositorie
migrate.clone_address=Clone adres migrate.clone_address=Clone adres
migrate.clone_address_desc=Dit kan een HTTP/HTTPS/GIT URL zijn of een lokaal pad. migrate.clone_address_desc=Dit kan een HTTP/HTTPS/GIT URL zijn of een lokaal pad.
migrate.permission_denied=You are not allowed to import local repositories.
migrate.invalid_local_path=Ongeldig lokaal pad, het pad bestaat niet of het is geen map. migrate.invalid_local_path=Ongeldig lokaal pad, het pad bestaat niet of het is geen map.
forked_from=geforked van forked_from=geforked van
@ -498,10 +500,12 @@ pulls.reopen_to_merge=Please reopen this pull request to perform merge operation
pulls.merged=Merged pulls.merged=Merged
pulls.has_merged=This pull request has been merged successfully! pulls.has_merged=This pull request has been merged successfully!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information. pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request. pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits. pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it. pulls.cannot_auto_merge_helper=Please use command line tool to solve it.
pulls.merge_pull_request=Samenvoegen van pull verzoek pulls.merge_pull_request=Samenvoegen van pull verzoek
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
milestones.new=Nieuwe mijlpaal milestones.new=Nieuwe mijlpaal
milestones.open_tab=%d geopend milestones.open_tab=%d geopend
@ -516,7 +520,7 @@ milestones.title=Titel
milestones.desc=Beschrijving milestones.desc=Beschrijving
milestones.due_date=Vervaldatum (optioneel) milestones.due_date=Vervaldatum (optioneel)
milestones.clear=Leegmaken milestones.clear=Leegmaken
milestones.invalid_due_date_format=Formaat vervaldatum is ongeldig, moet zijn "jaar-mm-dd". milestones.invalid_due_date_format=Formaat vervaldatum is ongeldig, moet zijn "jjjj-mm-dd".
milestones.create_success=Mijlpaal '%s' is met succes aangemaakt! milestones.create_success=Mijlpaal '%s' is met succes aangemaakt!
milestones.edit=Bewerk mijlpaal milestones.edit=Bewerk mijlpaal
milestones.edit_subheader=Gebruik een goede beschrijving voor mijlpalen, om verwarring te voorkomen. milestones.edit_subheader=Gebruik een goede beschrijving voor mijlpalen, om verwarring te voorkomen.
@ -648,6 +652,7 @@ release.tag_name_already_exist=Versie met deze naam bestaat al.
[org] [org]
org_name_holder=Organisatienaam org_name_holder=Organisatienaam
org_full_name_holder=Organization Full Name
org_name_helper=Een goede organisatienaam is kort en memorabel. org_name_helper=Een goede organisatienaam is kort en memorabel.
create_org=Nieuwe organisatie aanmaken create_org=Nieuwe organisatie aanmaken
repo_updated=Geupdate repo_updated=Geupdate
@ -804,6 +809,7 @@ users.edit_account=Bewerk account
users.is_activated=Dit account is geactiveerd users.is_activated=Dit account is geactiveerd
users.is_admin=Dit account heeft beheerdersrechten users.is_admin=Dit account heeft beheerdersrechten
users.allow_git_hook=Deze account beschikt over machtigingen voor het maken van Git haken users.allow_git_hook=Deze account beschikt over machtigingen voor het maken van Git haken
users.allow_import_local=This account has permissions to import local repositories
users.update_profile=Account profiel bijwerken users.update_profile=Account profiel bijwerken
users.delete_account=Dit account verwijderen users.delete_account=Dit account verwijderen
users.still_own_repo=Dit account is nog steeds eigendom van een repositorie. U moet deze repositorie eerst verwijderen of overdragen. users.still_own_repo=Dit account is nog steeds eigendom van een repositorie. U moet deze repositorie eerst verwijderen of overdragen.

152
conf/locale/locale_pl-PL.ini

@ -13,7 +13,7 @@ version=Wersja
page=Strona page=Strona
template=Szablon template=Szablon
language=Język language=Język
create_new=Create... create_new=Utwórz...
user_profile_and_more=Profil użytkownika i więcej user_profile_and_more=Profil użytkownika i więcej
signed_in_as=Zalogowany jako signed_in_as=Zalogowany jako
@ -67,8 +67,8 @@ path=Ścieżka
sqlite_helper=The file path of SQLite3 or TiDB database. sqlite_helper=The file path of SQLite3 or TiDB database.
err_empty_db_path=SQLite3 or TiDB database path cannot be empty. err_empty_db_path=SQLite3 or TiDB database path cannot be empty.
err_invalid_tidb_name=TiDB database name does not allow characters "." and "-". err_invalid_tidb_name=TiDB database name does not allow characters "." and "-".
no_admin_and_disable_registration=You cannot disable registration without creating an admin account. no_admin_and_disable_registration=Rejestracji nie można wyłączyć bez tworzenia konta admina.
err_empty_admin_password=Admin password cannot be empty. err_empty_admin_password=Hasło admina nie może być pusta.
general_title=Ustawienia ogólne Gogs general_title=Ustawienia ogólne Gogs
app_name=Nazwa aplikacji app_name=Nazwa aplikacji
@ -102,7 +102,7 @@ disable_gravatar=Wyłącz usługę Gravatar
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default. disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default.
disable_registration=Wyłącz samodzielną rejestrację disable_registration=Wyłącz samodzielną rejestrację
disable_registration_popup=Wyłącz samodzielną rejestrację użytkownika, tylko administrator będzie mógł tworzyć konta. disable_registration_popup=Wyłącz samodzielną rejestrację użytkownika, tylko administrator będzie mógł tworzyć konta.
enable_captcha=Enable Captcha enable_captcha=Włącz Captcha
enable_captcha_popup=Require validate captcha for user self-registration. enable_captcha_popup=Require validate captcha for user self-registration.
require_sign_in_view=Włącz wymóg zalogowania do przeglądania stron require_sign_in_view=Włącz wymóg zalogowania do przeglądania stron
require_sign_in_view_popup=Tylko zalogowani użytkownicy będą mogli przeglądać strony, goście zobaczą tylko stronę logowania. require_sign_in_view_popup=Tylko zalogowani użytkownicy będą mogli przeglądać strony, goście zobaczą tylko stronę logowania.
@ -111,7 +111,7 @@ admin_title=Ustawienia konta administratora
admin_name=Nazwa Użytkownika admin_name=Nazwa Użytkownika
admin_password=Hasło admin_password=Hasło
confirm_password=Potwierdź hasło confirm_password=Potwierdź hasło
admin_email=E-mail admin_email=Admin E-mail
install_gogs=Zainstaluj Gogs install_gogs=Zainstaluj Gogs
test_git_failed=Nie udało się przetestować polecenia "git": %v test_git_failed=Nie udało się przetestować polecenia "git": %v
sqlite3_not_available=Twoje wydanie nie obsługuje SQLite3, proszę pobrać oficjalne wydanie z %s, a NIE wersję z gobuild. sqlite3_not_available=Twoje wydanie nie obsługuje SQLite3, proszę pobrać oficjalne wydanie z %s, a NIE wersję z gobuild.
@ -130,7 +130,7 @@ my_repos=Moje repozytoria
collaborative_repos=Wspólne repozytoria collaborative_repos=Wspólne repozytoria
my_orgs=Moje organizacje my_orgs=Moje organizacje
my_mirrors=Moje mirrory my_mirrors=Moje mirrory
view_home=View %s view_home=Zobacz %s
issues.in_your_repos=W twoich repozytoriach issues.in_your_repos=W twoich repozytoriach
@ -148,7 +148,6 @@ forgot_password=Zapomniałem hasła
forget_password=Zapomniałeś hasła? forget_password=Zapomniałeś hasła?
sign_up_now=Potrzebujesz konta? Zarejestruj się teraz. sign_up_now=Potrzebujesz konta? Zarejestruj się teraz.
confirmation_mail_sent_prompt=Nowa wiadomość e-mail z potwierdzeniem została wysłana do <b>%s</b>, proszę sprawdzić swoją skrzynkę odbiorczą w ciągu najbliższych godzin %d aby dokończyć proces rejestracji. confirmation_mail_sent_prompt=Nowa wiadomość e-mail z potwierdzeniem została wysłana do <b>%s</b>, proszę sprawdzić swoją skrzynkę odbiorczą w ciągu najbliższych godzin %d aby dokończyć proces rejestracji.
sign_in_to_account=Sign in to your account
active_your_account=Aktywuj swoje konto active_your_account=Aktywuj swoje konto
resent_limit_prompt=Niestety, zbyt często wysyłasz e-mail aktywacyjny. Proszę odczekać 3 minuty. resent_limit_prompt=Niestety, zbyt często wysyłasz e-mail aktywacyjny. Proszę odczekać 3 minuty.
has_unconfirmed_mail=Witaj, %s, masz niepotwierdzony adres e-mail (<b>%s</b>). Jeśli nie otrzymałeś wiadomości e-mail z potwierdzeniem lub potrzebujesz wysłać nową, kliknij na poniższy przycisk. has_unconfirmed_mail=Witaj, %s, masz niepotwierdzony adres e-mail (<b>%s</b>). Jeśli nie otrzymałeś wiadomości e-mail z potwierdzeniem lub potrzebujesz wysłać nową, kliknij na poniższy przycisk.
@ -161,10 +160,10 @@ reset_password_helper=Kliknij tutaj, aby zresetować hasło
password_too_short=Długość hasła nie może być mniejsza niż 6 znaków. password_too_short=Długość hasła nie może być mniejsza niż 6 znaków.
[mail] [mail]
activate_account=Please activate your account activate_account=Prosimy aktywować swoje konto
activate_email=Verify your e-mail address activate_email=Sprawdź Twój adres e-mail
reset_password=Reset your password reset_password=Zmień swoje hasło
register_success=Register success, Welcome register_success=Zostałeś zarejestrowany, witamy
[modal] [modal]
yes=Tak yes=Tak
@ -187,11 +186,12 @@ AdminEmail=E-mail administratora
require_error=` nie może być puste.` require_error=` nie może być puste.`
alpha_dash_error=` musi się składać z prawidłowych znaków alfanumerycznych, myślników oraz podkreśleń.` alpha_dash_error=` musi się składać z prawidłowych znaków alfanumerycznych, myślników oraz podkreśleń.`
alpha_dash_dot_error=` musi się składać z prawidłowych znaków alfanumerycznych, myślników, podkreśleń oraz kropek.` alpha_dash_dot_error=` musi się składać z prawidłowych znaków alfanumerycznych, myślników, podkreśleń oraz kropek.`
size_error=` must be size %s.` size_error="musi być wielkości %s."
min_size_error=` musi zawierać co najwyżej %s znaków.` min_size_error=` musi zawierać co najwyżej %s znaków.`
max_size_error=` musi zawierać co najwyżej %s znaków.` max_size_error=` musi zawierać co najwyżej %s znaków.`
email_error=` nie jest poprawnym adresem e-mail.` email_error=` nie jest poprawnym adresem e-mail.`
url_error=` nie jest poprawnym adresem URL.` url_error=` nie jest poprawnym adresem URL.`
include_error=` must contain substring '%s'.`
unknown_error=Nieznany błąd: unknown_error=Nieznany błąd:
captcha_incorrect=Kod captcha nie zgadza się. captcha_incorrect=Kod captcha nie zgadza się.
password_not_match=Hasło i potwierdzenie nie zgadzają się. password_not_match=Hasło i potwierdzenie nie zgadzają się.
@ -267,7 +267,7 @@ update_avatar_success=Ustawienia awatarów zostały pomyślnie zaktualizowane.
change_password=Zmień hasło change_password=Zmień hasło
old_password=Aktualne hasło old_password=Aktualne hasło
new_password=Nowe hasło new_password=Nowe hasło
retype_new_password=Retype New Password retype_new_password=Powtórz nowe hasło
password_incorrect=Bieżące hasło nie jest prawidłowe. password_incorrect=Bieżące hasło nie jest prawidłowe.
change_password_success=Hasło zostało zmienione pomyślnie. Możesz teraz zalogować się za pomocą nowego hasła. change_password_success=Hasło zostało zmienione pomyślnie. Możesz teraz zalogować się za pomocą nowego hasła.
@ -277,9 +277,9 @@ email_desc=Twój podstawowy adres e-mail będzie używany dla powiadomień i inn
primary=Podstawowy primary=Podstawowy
primary_email=Ustaw jako podstawowy primary_email=Ustaw jako podstawowy
delete_email=Usuń delete_email=Usuń
email_deletion=E-mail Deletion email_deletion=Usunięcie wiadomości e-mail
email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue? email_deletion_desc=Delete this e-mail address will remove related information from your account. Do you want to continue?
email_deletion_success=E-mail has been deleted successfully! email_deletion_success=E-mail został usunięty pomyślnie!
add_new_email=Dodaj nowy e-mail add_new_email=Dodaj nowy e-mail
add_email=Dodaj e-mail add_email=Dodaj e-mail
add_email_confirmation_sent=Nowa wiadomość e-mail z potwierdzeniem została wysłana do '%s', proszę sprawdzić swoją skrzynkę odbiorczą w ciągu %d godzin, aby dokończyć proces potwierdzania. add_email_confirmation_sent=Nowa wiadomość e-mail z potwierdzeniem została wysłana do '%s', proszę sprawdzić swoją skrzynkę odbiorczą w ciągu %d godzin, aby dokończyć proces potwierdzania.
@ -291,14 +291,14 @@ ssh_desc=To jest lista kluczy SSH powiązanych z Twoim kontem. Usuń klucze, kt
ssh_helper=<strong>Potrzebujesz pomocy?</strong> Sprawdź nasz przewodnik <a href="%s"> generowania kluczy SSH</a> lub rozwiązywanie <a href="%s">typowych problemów z SSH</a>. ssh_helper=<strong>Potrzebujesz pomocy?</strong> Sprawdź nasz przewodnik <a href="%s"> generowania kluczy SSH</a> lub rozwiązywanie <a href="%s">typowych problemów z SSH</a>.
add_new_key=Dodaj klucz SSH add_new_key=Dodaj klucz SSH
ssh_key_been_used=Public key content has been used. ssh_key_been_used=Public key content has been used.
ssh_key_name_used=Public key with same name has already existed. ssh_key_name_used=Klucz publiczny o tej samej nazwie już istnieje.
key_name=Nazwa klucza key_name=Nazwa klucza
key_content=Treść key_content=Treść
add_key_success=New SSH key '%s' has been added successfully! add_key_success=Pomyślnie dodano nowy klucz SSH '%s'!
delete_key=Usuń delete_key=Usuń
ssh_key_deletion=Usunięcie klucza SSH ssh_key_deletion=Usunięcie klucza SSH
ssh_key_deletion_desc=Usunięcie tego klucza SSH będzie skutkować usunięciem wszystkich powiązanych dostępów do twojego konta. Czy chcesz kontynuować? ssh_key_deletion_desc=Usunięcie tego klucza SSH będzie skutkować usunięciem wszystkich powiązanych dostępów do twojego konta. Czy chcesz kontynuować?
ssh_key_deletion_success=SSH key has been deleted successfully! ssh_key_deletion_success=Klucz SSH został usunięty pomyślnie!
add_on=Dodano add_on=Dodano
last_used=Ostatnio użyto last_used=Ostatnio użyto
no_activity=Brak aktywności no_activity=Brak aktywności
@ -334,6 +334,7 @@ repo_name=Nazwa repozytorium
repo_name_helper=Dobre nazwy repozytorium są krótkie, wpadające w pamięć i <strong>unikalne</strong>. repo_name_helper=Dobre nazwy repozytorium są krótkie, wpadające w pamięć i <strong>unikalne</strong>.
visibility=Widoczność visibility=Widoczność
visiblity_helper=To repozytorium jest <span class="ui red text">prywatne</span> visiblity_helper=To repozytorium jest <span class="ui red text">prywatne</span>
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
visiblity_fork_helper=(Change of this value will affect all forks) visiblity_fork_helper=(Change of this value will affect all forks)
fork_repo=Sforkowane fork_repo=Sforkowane
fork_from=Forkuj z fork_from=Forkuj z
@ -344,7 +345,7 @@ repo_lang_helper=Wybierz pliki .gitignore
license=Licencja license=Licencja
license_helper=Wybierz plik licencji license_helper=Wybierz plik licencji
readme=Readme readme=Readme
readme_helper=Select a readme template readme_helper=Wybierz szablon readme
auto_init=Initialize this repository with selected files and template auto_init=Initialize this repository with selected files and template
create_repo=Utwórz repozytorium create_repo=Utwórz repozytorium
default_branch=Domyślna gałąź default_branch=Domyślna gałąź
@ -359,13 +360,14 @@ migrate_type_helper=This repository will be a <span class="text blue">mirror</sp
migrate_repo=Przenieś repozytorium migrate_repo=Przenieś repozytorium
migrate.clone_address=Sklonuj adres migrate.clone_address=Sklonuj adres
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path. migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path.
migrate.permission_denied=You are not allowed to import local repositories.
migrate.invalid_local_path=Ścieżka jest niepoprawna. Nie istnieje lub nie jest katalogiem. migrate.invalid_local_path=Ścieżka jest niepoprawna. Nie istnieje lub nie jest katalogiem.
forked_from=sklonowany z forked_from=sklonowany z
fork_from_self=You cannot fork repository you already owned! fork_from_self=You cannot fork repository you already owned!
copy_link=Kopiuj copy_link=Kopiuj
copy_link_success=Copied! copy_link_success=Skopiowane!
copy_link_error=Press ⌘-C or Ctrl-C to copy copy_link_error=Naciśnij klawisze ⌘-C i Ctrl-C, aby skopiować
click_to_copy=Kopiuj do schowka click_to_copy=Kopiuj do schowka
copied=Skopiowano copied=Skopiowano
clone_helper=Potrzebujesz pomocy z klonowaniem? Odwiedź <a target="_blank" href="%s">Pomoc</a>! clone_helper=Potrzebujesz pomocy z klonowaniem? Odwiedź <a target="_blank" href="%s">Pomoc</a>!
@ -380,7 +382,7 @@ quick_guide=Skrócona instrukcja
clone_this_repo=Klonuj repozytorium clone_this_repo=Klonuj repozytorium
create_new_repo_command=Utwórz nowe repozytorium z wiersza poleceń create_new_repo_command=Utwórz nowe repozytorium z wiersza poleceń
push_exist_repo=Wyślij istniejące repozytorium z wiersza poleceń push_exist_repo=Wyślij istniejące repozytorium z wiersza poleceń
repo_is_empty=This repository is empty, please come back later! repo_is_empty=To repozytorium jest puste, proszę wrócić później!
branch=Gałąź branch=Gałąź
@ -391,7 +393,7 @@ tags=Tagi
issues=Problemy issues=Problemy
pulls=Pull Requests pulls=Pull Requests
labels=Etykiety labels=Etykiety
milestones=Milestones milestones=Kamienie milowe
commits=Commity commits=Commity
releases=Wydania releases=Wydania
file_raw=Czysty file_raw=Czysty
@ -415,13 +417,13 @@ issues.new.clear_labels=Wyczyść etykiety
issues.new.milestone=Kamień milowy issues.new.milestone=Kamień milowy
issues.new.no_milestone=No Milestone issues.new.no_milestone=No Milestone
issues.new.clear_milestone=Clear milestone issues.new.clear_milestone=Clear milestone
issues.new.open_milestone=Open Milestones issues.new.open_milestone=Otwórz "kamienie milowe"
issues.new.closed_milestone=Closed Milestones issues.new.closed_milestone=Zamknięte "kamienie milowe"
issues.new.assignee=Assignee issues.new.assignee=Assignee
issues.new.clear_assignee=Clear assignee issues.new.clear_assignee=Clear assignee
issues.new.no_assignee=No assignee issues.new.no_assignee=No assignee
issues.create=Create Issue issues.create=Create Issue
issues.new_label=New Label issues.new_label=Nowa etykieta
issues.new_label_placeholder=Label name... issues.new_label_placeholder=Label name...
issues.create_label=Create Label issues.create_label=Create Label
issues.open_tab=%d Open issues.open_tab=%d Open
@ -453,10 +455,10 @@ issues.closed_title=zamknięty
issues.num_comments=%d komentarzy issues.num_comments=%d komentarzy
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=There is no content yet. issues.no_content=There is no content yet.
issues.close_issue=Close issues.close_issue=Zamknij
issues.close_comment_issue=Close and comment issues.close_comment_issue=Skomentuj i zamknij
issues.reopen_issue=Reopen issues.reopen_issue=Otwórz ponownie
issues.reopen_comment_issue=Reopen and comment issues.reopen_comment_issue=Otwórz ponownie i dodaj komentarz
issues.create_comment=Komentuj issues.create_comment=Komentuj
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -478,13 +480,13 @@ issues.label_delete=Usuń
issues.label_modify=Modyfikacja etykiety issues.label_modify=Modyfikacja etykiety
issues.label_deletion=Usunięcie etykiety issues.label_deletion=Usunięcie etykiety
issues.label_deletion_desc=Delete this label will remove its information in all related issues. Do you want to continue? issues.label_deletion_desc=Delete this label will remove its information in all related issues. Do you want to continue?
issues.label_deletion_success=Label has been deleted successfully! issues.label_deletion_success=Etykieta została usunięta pomyślnie!
pulls.compare_changes=Compare Changes pulls.compare_changes=Compare Changes
pulls.compare_changes_desc=Compare two branches and make a pull request for changes. pulls.compare_changes_desc=Compare two branches and make a pull request for changes.
pulls.compare_base=base pulls.compare_base=base
pulls.compare_compare=compare pulls.compare_compare=compare
pulls.filter_branch=Filter branch pulls.filter_branch=Filtruj branch
pulls.no_results=Nie znaleziono wyników. pulls.no_results=Nie znaleziono wyników.
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even. pulls.nothing_to_compare=There is nothing to compare because base and head branches are even.
pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>` pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
@ -492,16 +494,18 @@ pulls.create=Utwórz Pull Request
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code>
pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
pulls.tab_conversation=Conversation pulls.tab_conversation=Conversation
pulls.tab_commits=Commits pulls.tab_commits=Commity
pulls.tab_files=Files changed pulls.tab_files=Pliki zmodyfikowane
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation. pulls.reopen_to_merge=Please reopen this pull request to perform merge operation.
pulls.merged=Merged pulls.merged=Scalone
pulls.has_merged=This pull request has been merged successfully! pulls.has_merged=This pull request has been merged successfully!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information. pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request. pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits. pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it. pulls.cannot_auto_merge_helper=Please use command line tool to solve it.
pulls.merge_pull_request=Merge Pull Request pulls.merge_pull_request=Scal Pull Request
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
milestones.new=Nowy kamień milowy milestones.new=Nowy kamień milowy
milestones.open_tab=%d Open milestones.open_tab=%d Open
@ -511,17 +515,17 @@ milestones.no_due_date=Nie ustalono terminu
milestones.open=Otwórz milestones.open=Otwórz
milestones.close=Zamknij milestones.close=Zamknij
milestones.new_subheader=Create milestones to organize your issues. milestones.new_subheader=Create milestones to organize your issues.
milestones.create=Create Milestone milestones.create=Utwórz punkt kontrolny
milestones.title=Title milestones.title=Tytuł
milestones.desc=Description milestones.desc=Opis
milestones.due_date=Due Date (optional) milestones.due_date=Termin realizacji (opcjonalnie)
milestones.clear=Wyczyść milestones.clear=Wyczyść
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'. milestones.invalid_due_date_format=Format daty realizacji jest nieprawidłowy, musi być "rrrr-mm-dd".
milestones.create_success=Milestone '%s' has been created successfully! milestones.create_success=Kamień milowy "%s" został utworzony pomyślnie!
milestones.edit=Edit Milestone milestones.edit=Edytuj kamień milowy
milestones.edit_subheader=Use better description for milestones so people won't be confused. milestones.edit_subheader=Use better description for milestones so people won't be confused.
milestones.cancel=Cancel milestones.cancel=Anuluj
milestones.modify=Modify Milestone milestones.modify=Modyfikuj kamień milowy
milestones.edit_success=Changes of milestone '%s' has been saved successfully! milestones.edit_success=Changes of milestone '%s' has been saved successfully!
milestones.deletion=Milestone Deletion milestones.deletion=Milestone Deletion
milestones.deletion_desc=Delete this milestone will remove its information in all related issues. Do you want to continue? milestones.deletion_desc=Delete this milestone will remove its information in all related issues. Do you want to continue?
@ -580,12 +584,12 @@ settings.content_type=Typ zawartości
settings.secret=Sekret settings.secret=Sekret
settings.slack_username=Username settings.slack_username=Username
settings.slack_icon_url=Icon URL settings.slack_icon_url=Icon URL
settings.slack_color=Color settings.slack_color=Kolor
settings.event_desc=Jakie zdarzenia mają wywoływać ten skrypt internetowy? settings.event_desc=Jakie zdarzenia mają wywoływać ten skrypt internetowy?
settings.event_push_only=Tylko zdarzenia <code>push</code>. settings.event_push_only=Tylko zdarzenia <code>push</code>.
settings.event_send_everything=I need <strong>everything</strong>. settings.event_send_everything=Potrzebuję <strong>wszystkiego</strong>.
settings.event_choose=Let me choose what I need. settings.event_choose=Pozwól mi wybrać, czego potrzebuję.
settings.event_create=Create settings.event_create=Utwórz
settings.event_create_desc=Branch, or tag created settings.event_create_desc=Branch, or tag created
settings.event_push=Push settings.event_push=Push
settings.event_push_desc=Git push to a repository settings.event_push_desc=Git push to a repository
@ -604,8 +608,8 @@ settings.slack_channel=Kanał
settings.deploy_keys=Klucze wdrożeniowe settings.deploy_keys=Klucze wdrożeniowe
settings.add_deploy_key=Add Deploy Key settings.add_deploy_key=Add Deploy Key
settings.no_deploy_keys=You haven't added any deploy key. settings.no_deploy_keys=You haven't added any deploy key.
settings.title=Title settings.title=Tytuł
settings.deploy_key_content=Content settings.deploy_key_content=Treść
settings.key_been_used=Deploy key content has been used. settings.key_been_used=Deploy key content has been used.
settings.key_name_used=Deploy key with same name has already existed. settings.key_name_used=Deploy key with same name has already existed.
settings.add_key_success=New deploy key '%s' has been added successfully! settings.add_key_success=New deploy key '%s' has been added successfully!
@ -648,6 +652,7 @@ release.tag_name_already_exist=Wersja o tej nazwie tagu już istnieje.
[org] [org]
org_name_holder=Nazwa organizacji org_name_holder=Nazwa organizacji
org_full_name_holder=Organization Full Name
org_name_helper=Świetne nazwy organizacji są krótkie i łatwe do zapamiętania. org_name_helper=Świetne nazwy organizacji są krótkie i łatwe do zapamiętania.
create_org=Utwórz organizację create_org=Utwórz organizację
repo_updated=Zaktualizowano repo_updated=Zaktualizowano
@ -731,9 +736,9 @@ authentication=Uwierzytelnienia
config=Konfiguracja config=Konfiguracja
notices=Powiadomienia systemowe notices=Powiadomienia systemowe
monitor=Monitorowanie monitor=Monitorowanie
first_page=First first_page=Pierwsza
last_page=Last last_page=Ostatnia
total=Total: %d total=Ogółem: %d
dashboard.statistic=Statystyki dashboard.statistic=Statystyki
dashboard.operations=Operacje dashboard.operations=Operacje
@ -804,6 +809,7 @@ users.edit_account=Edytuj konto
users.is_activated=To konto jest aktywne users.is_activated=To konto jest aktywne
users.is_admin=To konto ma uprawnienia administratora users.is_admin=To konto ma uprawnienia administratora
users.allow_git_hook=To konto posiada uprawnienia do tworzenia skryptów Git users.allow_git_hook=To konto posiada uprawnienia do tworzenia skryptów Git
users.allow_import_local=This account has permissions to import local repositories
users.update_profile=Zaktualizuj profil konta users.update_profile=Zaktualizuj profil konta
users.delete_account=Usuń to konto users.delete_account=Usuń to konto
users.still_own_repo=Twoje konto jest dalej właścicielem repozytorium, musisz je usunąć lub przekazać. users.still_own_repo=Twoje konto jest dalej właścicielem repozytorium, musisz je usunąć lub przekazać.
@ -824,13 +830,13 @@ repos.stars=Polubienia
repos.issues=Problemy repos.issues=Problemy
auths.auth_manage_panel=Authentication Manage Panel auths.auth_manage_panel=Authentication Manage Panel
auths.new=Add New Source auths.new=Dodać nowe Źródło
auths.name=Nazwa auths.name=Nazwa
auths.type=Typ auths.type=Typ
auths.enabled=Włączono auths.enabled=Włączono
auths.updated=Zaktualizowano auths.updated=Zaktualizowano
auths.auth_type=Authentication Type auths.auth_type=Typ uwierzytelniania
auths.auth_name=Authentication Name auths.auth_name=Nazwa uwierzytelniania
auths.domain=Domena auths.domain=Domena
auths.host=Host auths.host=Host
auths.port=Port auths.port=Port
@ -848,22 +854,22 @@ auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=SMTP Authentication Type auths.smtp_auth=SMTP Authentication Type
auths.smtphost=Serwer SMTP auths.smtphost=Serwer SMTP
auths.smtpport=Port SMTP auths.smtpport=Port SMTP
auths.allowed_domains=Allowed Domains auths.allowed_domains=Dozwolone domeny
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','. auths.allowed_domains_helper=Pozostaw puste aby nie ograniczać domen. Wiele domen powinno być oddzielone przecinkami ','.
auths.enable_tls=Włącz szyfrowanie TLS auths.enable_tls=Włącz szyfrowanie TLS
auths.skip_tls_verify=Pomiń weryfikację protokołu TLS auths.skip_tls_verify=Pomiń weryfikację protokołu TLS
auths.pam_service_name=Nazwa usługi PAM auths.pam_service_name=Nazwa usługi PAM
auths.enable_auto_register=Włącz automatyczną rejestrację auths.enable_auto_register=Włącz automatyczną rejestrację
auths.tips=Wskazówki auths.tips=Wskazówki
auths.edit=Edit Authentication Setting auths.edit=Edytuj ustawienia uwierzytelniania
auths.activated=To uwierzytelnienie zostało aktywowane auths.activated=To uwierzytelnienie zostało aktywowane
auths.new_success=New authentication '%s' has been added successfully. auths.new_success=Pomyślnie dodano nowe uwierzytelnianie '%s'.
auths.update_success=Authentication setting has been updated successfully. auths.update_success=Ustawienia uwierzytelnienia zostały zaktualizowane pomyślnie.
auths.update=Update Authentication Setting auths.update=Aktualizuj ustawienia uwierzytelniania
auths.delete=Delete This Authentication auths.delete=Usuń to uwierzytelnianie
auths.delete_auth_title=Authentication Deletion auths.delete_auth_title=Usunięcie uwierzytelniania
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue? auths.delete_auth_desc=To uwierzytelnienie zostanie usunięte, czy chcesz kontynuować?
auths.deletion_success=Authentication has been deleted successfully! auths.deletion_success=Uwierzytelnianie zostało usunięte pomyślnie!
config.server_config=Konfiguracja serwera config.server_config=Konfiguracja serwera
config.app_name=Nazwa Aplikacji config.app_name=Nazwa Aplikacji
@ -887,7 +893,7 @@ config.db_user=Użytkownik
config.db_ssl_mode=Tryb SSL config.db_ssl_mode=Tryb SSL
config.db_ssl_mode_helper=(tylko dla "postgres") config.db_ssl_mode_helper=(tylko dla "postgres")
config.db_path=Ścieżka config.db_path=Ścieżka
config.db_path_helper=(for "sqlite3" and "tidb") config.db_path_helper=(dla "sqlite3" i "tidb")
config.service_config=Konfiguracja usługi config.service_config=Konfiguracja usługi
config.register_email_confirm=Wymagaj potwierdzenia e-mail config.register_email_confirm=Wymagaj potwierdzenia e-mail
config.disable_register=Wyłącz rejestrację config.disable_register=Wyłącz rejestrację
@ -895,8 +901,8 @@ config.show_registration_button=Pokazuj przycisk rejestracji
config.require_sign_in_view=Wymagaj bycia zalogowanym config.require_sign_in_view=Wymagaj bycia zalogowanym
config.enable_cache_avatar=Włącz cache awatarów config.enable_cache_avatar=Włącz cache awatarów
config.mail_notify=Powiadomienia e-mail config.mail_notify=Powiadomienia e-mail
config.disable_key_size_check=Disable Minimum Key Size Check config.disable_key_size_check=Wyłącz sprawdzanie minimalnego rozmiaru klucza
config.enable_captcha=Enable Captcha config.enable_captcha=Włącz Captcha
config.active_code_lives=Ważność kodów aktywacyjnych config.active_code_lives=Ważność kodów aktywacyjnych
config.reset_password_code_lives=Czas życia kodu resetowania hasła config.reset_password_code_lives=Czas życia kodu resetowania hasła
config.webhook_config=Konfiguracja skryptów internetowych config.webhook_config=Konfiguracja skryptów internetowych
@ -950,12 +956,12 @@ notices.delete_success=Powiadomienia systemowe zostały usunięte pomyślnie.
[action] [action]
create_repo=utworzono repozytorium <a href="%s"> %s</a> create_repo=utworzono repozytorium <a href="%s"> %s</a>
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a> rename_repo=nazwa repozytorium zmieniona z <code>%[1]s</code> na <a href="%[2]s">%[3]s</a>
commit_repo=wypchnął do <a href="%s/src/%s">%[2]s</a> w <a href="%[1]s"> %[3]s</a> commit_repo=wypchnął do <a href="%s/src/%s">%[2]s</a> w <a href="%[1]s"> %[3]s</a>
create_issue=`zgłosił problem <a href="%s/issues/%s">#%[2]s %[3]s</a>` create_issue=`zgłosił problem <a href="%s/issues/%s">#%[2]s %[3]s</a>`
create_pull_request=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>` create_pull_request="stworzył pull request <a href="%s/pulls/%s"> %s #%[2]s"</a>
comment_issue=`skomentował problem <a href="%s/issues/%s">#%[2]s %[3]s</a>` comment_issue=`skomentował problem <a href="%s/issues/%s">#%[2]s %[3]s</a>`
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>` merge_pull_request=scalił pull request <a href="%s/pulls/%s"> %s #%[2]s"</a>
transfer_repo=przeniósł repozytorium <code>%s</code> do <a href="%s">%s</a> transfer_repo=przeniósł repozytorium <code>%s</code> do <a href="%s">%s</a>
push_tag=opublikował tag <a href="%s/src/%s">%[2]s</a> w <a href="%[1]s">%[3]s</a> push_tag=opublikował tag <a href="%s/src/%s">%[2]s</a> w <a href="%[1]s">%[3]s</a>
compare_2_commits=Zobacz porównanie tych 2 commitów compare_2_commits=Zobacz porównanie tych 2 commitów

194
conf/locale/locale_pt-BR.ini

@ -53,7 +53,7 @@ code=Código
[install] [install]
install=Instalação install=Instalação
title=Etapas de instalação para Primeira Execução title=Etapas de instalação para Primeira Execução
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page! docker_helper=Se você está rodando o Gogs dentro do Docker, por favor leia os <a target="_blank" href="%s">Guias</a> cuidadosamente antes de mudar qualquer coisa nesta página!
requite_db_desc=Gogs requer MySQL, PostgreSQL, SQLite3 ou TiDB. requite_db_desc=Gogs requer MySQL, PostgreSQL, SQLite3 ou TiDB.
db_title=Configurações de Banco de Dados db_title=Configurações de Banco de Dados
db_type=Tipo do Banco de Dados db_type=Tipo do Banco de Dados
@ -102,16 +102,16 @@ disable_gravatar=Desativar Serviço Gravatar
disable_gravatar_popup=Desabilitar o Gravatar e fontes personalizadas, todos os avatares são enviados por usuários ou padrão. disable_gravatar_popup=Desabilitar o Gravatar e fontes personalizadas, todos os avatares são enviados por usuários ou padrão.
disable_registration=Desativar auto-registro disable_registration=Desativar auto-registro
disable_registration_popup=Desativar o auto-registro de usuário, para que somente o administrador possa criar contas. disable_registration_popup=Desativar o auto-registro de usuário, para que somente o administrador possa criar contas.
enable_captcha=Enable Captcha enable_captcha=Habilitar Captcha
enable_captcha_popup=Require validate captcha for user self-registration. enable_captcha_popup=Obrigar validação por captcha para auto-registro de usuários.
require_sign_in_view=Requerer autenticação para a visualização de páginas require_sign_in_view=Requerer login para a visualização de páginas
require_sign_in_view_popup=Somente usuários autenticados podem ver todas as páginas, visitantes somente podem entrar ou se cadastrar. require_sign_in_view_popup=Somente usuários autenticados podem ver todas as páginas, visitantes somente podem entrar ou se cadastrar.
admin_setting_desc=Você não precisa criar uma conta de administrador agora, no entanto o primeiro usuário (ID=1) automaticamente terá acesso de administrador. admin_setting_desc=Você não precisa criar uma conta de administrador agora, no entanto o primeiro usuário (ID=1) automaticamente terá acesso de administrador.
admin_title=Configurações da Conta de Administrador admin_title=Configurações da Conta de Administrador
admin_name=Nome de Usuário admin_name=Nome de Usuário
admin_password=Senha admin_password=Senha
confirm_password=Confirmar Senha confirm_password=Confirmar Senha
admin_email=E-mail admin_email=E-mail do Administrador
install_gogs=Instalar Gogs install_gogs=Instalar Gogs
test_git_failed=Falha ao testar o comando 'git': %v test_git_failed=Falha ao testar o comando 'git': %v
sqlite3_not_available=Sua versão não suporta SQLite3, por favor faça o download da versão binária oficial em %s, NÃO da versão gobuild. sqlite3_not_available=Sua versão não suporta SQLite3, por favor faça o download da versão binária oficial em %s, NÃO da versão gobuild.
@ -142,13 +142,12 @@ create_new_account=Criar Nova Conta
register_hepler_msg=Já tem uma conta? Entre agora! register_hepler_msg=Já tem uma conta? Entre agora!
social_register_hepler_msg=Já tem uma conta? Junte-se agora! social_register_hepler_msg=Já tem uma conta? Junte-se agora!
disable_register_prompt=Desculpe, novos registros estão desabilitados. Por favor entre em contato com o administrador do site. disable_register_prompt=Desculpe, novos registros estão desabilitados. Por favor entre em contato com o administrador do site.
disable_register_mail=Desculpe, a confirmação de registro por email foi desabilitada. disable_register_mail=Desculpe, a confirmação de registro por e-mail foi desabilitada.
remember_me=Lembrar de Mim remember_me=Lembrar de Mim
forgot_password=Esqueci a Senha forgot_password=Esqueci a Senha
forget_password=Esqueceu a senha? forget_password=Esqueceu a senha?
sign_up_now=Precisa de uma conta? Cadastre-se agora. sign_up_now=Precisa de uma conta? Cadastre-se agora.
confirmation_mail_sent_prompt=Um novo e-mail de confirmação foi enviado para <b>%s</b>, por favor, verifique sua caixa de entrada nas próximas %d horas para completar seu registro. confirmation_mail_sent_prompt=Um novo e-mail de confirmação foi enviado para <b>%s</b>, por favor, verifique sua caixa de entrada nas próximas %d horas para completar seu registro.
sign_in_to_account=Sign in to your account
active_your_account=Ativar Sua Conta active_your_account=Ativar Sua Conta
resent_limit_prompt=Desculpe, você está enviando um e-mail de ativação com muita frequência. Por favor, aguarde 3 minutos. resent_limit_prompt=Desculpe, você está enviando um e-mail de ativação com muita frequência. Por favor, aguarde 3 minutos.
has_unconfirmed_mail=Oi %s, você possui um endereço de e-mail não confirmado (<b>%s</b>). Se você não recebeu um e-mail de confirmação ou precisa reenviar um novo, clique no botão abaixo. has_unconfirmed_mail=Oi %s, você possui um endereço de e-mail não confirmado (<b>%s</b>). Se você não recebeu um e-mail de confirmação ou precisa reenviar um novo, clique no botão abaixo.
@ -161,10 +160,10 @@ reset_password_helper=Clique aqui para redefinir sua senha
password_too_short=O comprimento da senha não pode ser menor que 6. password_too_short=O comprimento da senha não pode ser menor que 6.
[mail] [mail]
activate_account=Please activate your account activate_account=Por favor, ative sua conta
activate_email=Verify your e-mail address activate_email=Verifique seu endereço de e-mail
reset_password=Reset your password reset_password=Resetar sua senha
register_success=Register success, Welcome register_success=Registrado com sucesso. Bem vindo
[modal] [modal]
yes=Sim yes=Sim
@ -187,26 +186,27 @@ AdminEmail=E-mail do Administrador
require_error=` não pode estar vazio.` require_error=` não pode estar vazio.`
alpha_dash_error=` devem ser caracteres alfanuméricos ou hífen (-) ou sublinhado (_).` alpha_dash_error=` devem ser caracteres alfanuméricos ou hífen (-) ou sublinhado (_).`
alpha_dash_dot_error=` devem ser caracteres alfanuméricos ou hífen (-) ou sublinhado (_).` alpha_dash_dot_error=` devem ser caracteres alfanuméricos ou hífen (-) ou sublinhado (_).`
size_error=` deve ter %s.` size_error='deve ser o tamanho %s.'
min_size_error=` deve conter pelo menos %s caracteres.` min_size_error=` deve conter pelo menos %s caracteres.`
max_size_error=` deve conter no máximo %s caracteres.` max_size_error=` deve conter no máximo %s caracteres.`
email_error=` não é um endereço de e-mail válido.` email_error=` não é um endereço de e-mail válido.`
url_error=`não é uma URL válida.` url_error=`não é uma URL válida.`
include_error=` deve conter '%s'.`
unknown_error=Erro desconhecido: unknown_error=Erro desconhecido:
captcha_incorrect=O captcha não correspondeu. captcha_incorrect=O captcha não correspondeu.
password_not_match=Senha e confirmar senha não são as mesmas. password_not_match=Senha e confirmação da senha não são as mesmas.
username_been_taken=Nome de usuário já foi tomado. username_been_taken=Nome de usuário já foi tomado.
repo_name_been_taken=Nome do repositório já foi tomado. repo_name_been_taken=Nome do repositório já foi tomado.
org_name_been_taken=Nome da organização já foi tomado. org_name_been_taken=Nome da organização já foi tomado.
team_name_been_taken=Nome da equipe já foi tomado. team_name_been_taken=Nome da equipe já foi tomado.
email_been_used=Endereço de e-mail já foi usado. email_been_used=Endereço de e-mail já foi usado.
illegal_team_name=O nome da equipe contém caracteres ilegais. illegal_team_name=O nome da equipe contém caracteres não permitidos.
username_password_incorrect=Usuário ou senha incorretos. username_password_incorrect=Usuário ou senha incorretos.
enterred_invalid_repo_name=Por favor certifique-se que informou o nome do repositório corretamente. enterred_invalid_repo_name=Por favor certifique-se que informou o nome do repositório corretamente.
enterred_invalid_owner_name=Por favor, verifique se o nome do proprietário está correto. enterred_invalid_owner_name=Por favor, verifique se o nome do proprietário está correto.
enterred_invalid_password=Por favor, verifique se a senha que você digitou está correta. enterred_invalid_password=Por favor, verifique se a senha que você digitou está correta.
user_not_exist=O usuário dado não existe. user_not_exist=O usuário informado não existe.
last_org_owner=O usuário a ser removido é o último membro na equipe de proprietários. Deve haver um outro proprietário. last_org_owner=O usuário a ser removido é o último membro na equipe de proprietários. Deve haver um outro proprietário.
invalid_ssh_key=Desculpe, não conseguimos verificar a sua chave SSH: %s invalid_ssh_key=Desculpe, não conseguimos verificar a sua chave SSH: %s
@ -219,7 +219,7 @@ org_still_own_repo=Esta organização ainda tem a propriedade do repositório, v
still_own_user=Esta autenticação ainda é usada por alguns usuários, você deve movê-los e depois apagar novamente. still_own_user=Esta autenticação ainda é usada por alguns usuários, você deve movê-los e depois apagar novamente.
target_branch_not_exist=O ramo de destino não existe. target_branch_not_exist=O branch de destino não existe.
[user] [user]
change_avatar=Altere o seu avatar em gravatar.com change_avatar=Altere o seu avatar em gravatar.com
@ -228,7 +228,7 @@ join_on=Inscreveu-se em
repositories=Repositórios repositories=Repositórios
activity=Atividade Pública activity=Atividade Pública
followers=Seguidores followers=Seguidores
starred=Marcado starred=Favorito
following=Seguindo following=Seguindo
form.name_reserved=O nome de usuário '%s' não pode ser usado. form.name_reserved=O nome de usuário '%s' não pode ser usado.
@ -252,7 +252,7 @@ location=Localização
update_profile=Atualizar o Perfil update_profile=Atualizar o Perfil
update_profile_success=O seu perfil foi atualizado com sucesso. update_profile_success=O seu perfil foi atualizado com sucesso.
change_username=Nome de Usuário Alterado change_username=Nome de Usuário Alterado
change_username_prompt=Essa alteração afetará o modo como ligações referem-se à sua conta. change_username_prompt=Essa alteração afetará os links para a sua conta.
continue=Continuar continue=Continuar
cancel=Cancelar cancel=Cancelar
@ -273,7 +273,7 @@ change_password_success=A senha está alterada com sucesso. Você pode agora ent
emails=Endereços de E-mail emails=Endereços de E-mail
manage_emails=Gerenciar endereços de e-mail manage_emails=Gerenciar endereços de e-mail
email_desc=Seu endereço de email principal será usado para notificações e outras operações. email_desc=Seu endereço de e-mail principal será usado para notificações e outras operações.
primary=Principal primary=Principal
primary_email=Definir como principal primary_email=Definir como principal
delete_email=Deletar delete_email=Deletar
@ -287,7 +287,7 @@ add_email_success=Seu novo endereço de E-mail foi adicionado com sucesso.
manage_ssh_keys=Gerenciar Chaves SSH manage_ssh_keys=Gerenciar Chaves SSH
add_key=Adicionar chave add_key=Adicionar chave
ssh_desc=Esta é uma lista de chaves SSH associadas com a sua conta. Remova quaisquer chaves que você não reconheça. ssh_desc=Esta é uma lista de chaves SSH associadas com a sua conta. Como essas chaves permitem que qualquer um que as usem tenham acesso aos seus repositórios, é altamente importante que você reconheça elas.
ssh_helper=<strong>Precisa de ajuda?</strong> Confira nosso guia para <a href="%s">gerar chaves SSH</a> ou solucionar <a href="%s">problemas comuns com SSH</a>. ssh_helper=<strong>Precisa de ajuda?</strong> Confira nosso guia para <a href="%s">gerar chaves SSH</a> ou solucionar <a href="%s">problemas comuns com SSH</a>.
add_new_key=Adicionar Chave SSH add_new_key=Adicionar Chave SSH
ssh_key_been_used=Uma chave pública com esse mesmo conteúdo já está em uso. ssh_key_been_used=Uma chave pública com esse mesmo conteúdo já está em uso.
@ -331,23 +331,24 @@ delete_account_desc=Esta conta será deletada permanentemente, você quer contin
[repo] [repo]
owner=Dono owner=Dono
repo_name=Nome do Repositório repo_name=Nome do Repositório
repo_name_helper=Nomes de repositórios bons são pequenos, memorizáveis e <strong>únicos</strong>. repo_name_helper=Nomes de repositórios bons são pequenos, memorizáveis e únicos.
visibility=Visibilidade visibility=Visibilidade
visiblity_helper=Este é um repositório <span class="ui red text"> privado</span> visiblity_helper=Este é um repositório <span class="ui red text"> privado</span>
visiblity_helper_forced=O adminstrador forçou todos os novos repositórios para serem <span class="ui red text">Privados</span>
visiblity_fork_helper=(A alteração desse valor irá afetar todos os forks) visiblity_fork_helper=(A alteração desse valor irá afetar todos os forks)
fork_repo=Fork o Repositório fork_repo=Fork o Repositório
fork_from=Fork de fork_from=Fork de
fork_visiblity_helper=Não é possível alterar a visibilidade de um repositório bifurcado fork_visiblity_helper=Não é possível alterar a visibilidade de um repositório forkado.
repo_desc=Descrição repo_desc=Descrição
repo_lang=Idioma repo_lang=Linguagem
repo_lang_helper=Selecione arquivos .gitignore repo_lang_helper=Selecione arquivos .gitignore
license=Licença license=Licença
license_helper=Selecione um arquivo de licença license_helper=Selecione um arquivo de licença
readme=Leia-me readme=Leia-me
readme_helper=Selecione um modelo de leiame readme_helper=Selecione um modelo de leia-me
auto_init=Inicializar este repositório com os arquivos selecionados e modelo auto_init=Inicializar este repositório com os arquivos selecionados e modelo
create_repo=Criar Repositório create_repo=Criar Repositório
default_branch=Ramo padrão default_branch=Branch padrão
mirror_interval=Intervalo de Espelho (hora) mirror_interval=Intervalo de Espelho (hora)
form.name_reserved=O nome de repositório '%s' não pode ser usado. form.name_reserved=O nome de repositório '%s' não pode ser usado.
@ -359,13 +360,14 @@ migrate_type_helper=Este repositório será um <span class="text blue"> espelho<
migrate_repo=Migrar Repositório migrate_repo=Migrar Repositório
migrate.clone_address=Endereço de Clone migrate.clone_address=Endereço de Clone
migrate.clone_address_desc=Isto pode ser uma URL de HTTP/HTTPS/GIT ou um caminho de diretório local. migrate.clone_address_desc=Isto pode ser uma URL de HTTP/HTTPS/GIT ou um caminho de diretório local.
migrate.permission_denied=Você não pode importar repositórios locais.
migrate.invalid_local_path=Caminho local inválido, não existe ou não é um diretório. migrate.invalid_local_path=Caminho local inválido, não existe ou não é um diretório.
forked_from=bifurcação de forked_from=forkado de
fork_from_self=Você não pode criar fork de um repositório que já é seu! fork_from_self=Você não pode criar fork de um repositório que já é seu!
copy_link=Copiar copy_link=Copiar
copy_link_success=Copied! copy_link_success=Copiado!
copy_link_error=Press ⌘-C or Ctrl-C to copy copy_link_error=Pressione ⌘-C ou Ctrl-C para copiar
click_to_copy=Copiar para a área de transferência click_to_copy=Copiar para a área de transferência
copied=Copiado com sucesso copied=Copiado com sucesso
clone_helper=Precisa de ajuda com a clonagem? Visite a <a target="_blank" href="%s">Ajuda</a>! clone_helper=Precisa de ajuda com a clonagem? Visite a <a target="_blank" href="%s">Ajuda</a>!
@ -380,20 +382,20 @@ quick_guide=Guia Rápido
clone_this_repo=Clonar este repositório clone_this_repo=Clonar este repositório
create_new_repo_command=Criar um novo repositório na linha de comando create_new_repo_command=Criar um novo repositório na linha de comando
push_exist_repo=Push um repositório existente na linha de comando push_exist_repo=Push um repositório existente na linha de comando
repo_is_empty=This repository is empty, please come back later! repo_is_empty=Este repositório está vazio, por favor volte mais tarde!
branch=Ramo branch=Branch
tree=Árvore tree=Árvore
branch_and_tags=Ramos & Tags branch_and_tags=Branches & Tags
branches=Ramos branches=Branches
tags=Tags tags=Tags
issues=Problemas issues=Problemas
pulls=Pull Requests pulls=Pull Requests
labels=Etiquetas labels=Etiquetas
milestones=Marcos milestones=Marcos
commits=Commits commits=Commits
releases=Lançamentos releases=Versões
file_raw=Cru file_raw=Cru
file_history=Histórico file_history=Histórico
file_view_raw=Ver cru file_view_raw=Ver cru
@ -412,7 +414,7 @@ issues.new=Novo problema
issues.new.labels=Etiquetas issues.new.labels=Etiquetas
issues.new.no_label=Sem etiqueta issues.new.no_label=Sem etiqueta
issues.new.clear_labels=Limpar issues.new.clear_labels=Limpar
issues.new.milestone=Milestone issues.new.milestone=Marco
issues.new.no_milestone=Sem marco issues.new.no_milestone=Sem marco
issues.new.clear_milestone=Limpar issues.new.clear_milestone=Limpar
issues.new.open_milestone=Marcos abertos issues.new.open_milestone=Marcos abertos
@ -422,7 +424,7 @@ issues.new.clear_assignee=Limpar
issues.new.no_assignee=Não atribuída issues.new.no_assignee=Não atribuída
issues.create=Salvar issues.create=Salvar
issues.new_label=Nova etiqueta issues.new_label=Nova etiqueta
issues.new_label_placeholder=Nome de etiqueta... issues.new_label_placeholder=Nome da etiqueta...
issues.create_label=Salvar issues.create_label=Salvar
issues.open_tab=%d aberto issues.open_tab=%d aberto
issues.close_tab=%d fechados issues.close_tab=%d fechados
@ -435,7 +437,7 @@ issues.filter_assginee_no_select=Sem atribuição
issues.filter_type=Tipo issues.filter_type=Tipo
issues.filter_type.all_issues=Todos os problemas issues.filter_type.all_issues=Todos os problemas
issues.filter_type.assigned_to_you=Atribuídos a você issues.filter_type.assigned_to_you=Atribuídos a você
issues.filter_type.created_by_you=Criados por você issues.filter_type.created_by_you=Criado por você
issues.filter_type.mentioning_you=Mencionando você issues.filter_type.mentioning_you=Mencionando você
issues.filter_sort=Ordenação issues.filter_sort=Ordenação
issues.filter_sort.latest=Mais novos issues.filter_sort.latest=Mais novos
@ -444,7 +446,7 @@ issues.filter_sort.recentupdate=Mais recentemente atualizados
issues.filter_sort.leastupdate=Menos recentemente atualizados issues.filter_sort.leastupdate=Menos recentemente atualizados
issues.filter_sort.mostcomment=Mais comentados issues.filter_sort.mostcomment=Mais comentados
issues.filter_sort.leastcomment=Menos comentados issues.filter_sort.leastcomment=Menos comentados
issues.opened_by=%[1]s foi aberto por <a href="/%[2]s">%[3]s</a> issues.opened_by=%[1]s foi aberto por <a href="%[2]s">%[3]s</a>
issues.opened_by_fake=aberto %[1]s por %[2]s issues.opened_by_fake=aberto %[1]s por %[2]s
issues.previous=Página anterior issues.previous=Página anterior
issues.next=Próxima página issues.next=Próxima página
@ -456,12 +458,12 @@ issues.no_content=Nenhum conteúdo textual.
issues.close_issue=Fechar issues.close_issue=Fechar
issues.close_comment_issue=Comentar e fechar issues.close_comment_issue=Comentar e fechar
issues.reopen_issue=Reabrir issues.reopen_issue=Reabrir
issues.reopen_comment_issue=Reabrir e comentar issues.reopen_comment_issue=Comentar e reabrir
issues.create_comment=Comentar issues.create_comment=Comentar
issues.closed_at=`fechado em <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`fechado em <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`reaberto em <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`reaberto em <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at=`citou este problema em um commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.poster=Imagem issues.poster=Autor
issues.admin=Administrador issues.admin=Administrador
issues.owner=Proprietário issues.owner=Proprietário
issues.sign_up_for_free=Cadastre-se gratuitamente issues.sign_up_for_free=Cadastre-se gratuitamente
@ -481,27 +483,29 @@ issues.label_deletion_desc=Excluir uma etiqueta a retirará de todos os problema
issues.label_deletion_success=A etiqueta foi excluída com sucesso! issues.label_deletion_success=A etiqueta foi excluída com sucesso!
pulls.compare_changes=Comparar mudanças pulls.compare_changes=Comparar mudanças
pulls.compare_changes_desc=Comparar dois ramos e criar solicitação de pull com as mudanças. pulls.compare_changes_desc=Comparar os dois branches e criar pull request com as mudanças.
pulls.compare_base=base pulls.compare_base=base
pulls.compare_compare=comparar pulls.compare_compare=comparar
pulls.filter_branch=Filtrar branch pulls.filter_branch=Filtrar branch
pulls.no_results=Nada encontrado. pulls.no_results=Nada encontrado.
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even. pulls.nothing_to_compare=Não há nada para comparar porque o branch base e o head estão iguais.
pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>` pulls.has_pull_request=`Já existem pull requests entre esses dois alvos: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
pulls.create=Criar Pull Request pulls.create=Criar Pull Request
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> pulls.title_desc=quer mesclar %[1]d commits de <code>%[2]s</code> em <code>%[3]s</code>
pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s pulls.merged_title_desc=mesclou %[1]d commits de <code>%[2]s</code> em <code>%[3]s</code> %[4]s
pulls.tab_conversation=Conversação pulls.tab_conversation=Conversação
pulls.tab_commits=Commits pulls.tab_commits=Commits
pulls.tab_files=Arquivos alterados pulls.tab_files=Arquivos alterados
pulls.reopen_to_merge=Por favor reabra esse pull request para executar a operação de merge. pulls.reopen_to_merge=Por favor reabra esse pull request para executar a operação de mescla.
pulls.merged=Merge realizado pulls.merged=Merge realizado
pulls.has_merged=This pull request has been merged successfully! pulls.has_merged=Este pull request foi mesclado com sucesso!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information. pulls.data_broken=Dados deste pull request foram quebrados devido à deleção de informação do fork.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request. pulls.is_checking=A verificação do conflito ainda está em progresso, por favor recarregue a página em instantes.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits. pulls.can_auto_merge_desc=Você pode realizar uma auto-mescla neste pull request.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it. pulls.cannot_auto_merge_desc=Você não pode realizar uma auto-mescla porque há conflitos entre os commits.
pulls.cannot_auto_merge_helper=Por favor, utilize linha de comando para solucionar isto.
pulls.merge_pull_request=Merge Pull Request pulls.merge_pull_request=Merge Pull Request
pulls.open_unmerged_pull_exists=' Você não pode executar a operação de reabrir porque já existe uma solicitação de pull aberta (#%d) do mesmo repositório com as mesmas informações de merge e está esperando pelo merge.'
milestones.new=Novo marco milestones.new=Novo marco
milestones.open_tab=%d abertos milestones.open_tab=%d abertos
@ -530,26 +534,26 @@ milestones.deletion_success=Marco excluído com sucesso!
settings=Configurações settings=Configurações
settings.options=Opções settings.options=Opções
settings.collaboration=Colaboração settings.collaboration=Colaboração
settings.hooks=Hooks da web settings.hooks=Webhooks
settings.githooks=Hooks do Git settings.githooks=Hooks do Git
settings.basic_settings=Configurações Básicas settings.basic_settings=Configurações Básicas
settings.danger_zone=Zona de Perigo settings.danger_zone=Zona de Perigo
settings.site=Site Oficial settings.site=Site Oficial
settings.update_settings=Configurações de Atualização settings.update_settings=Configurações de Atualização
settings.change_reponame_prompt=This change will affect how links relate to the repository. settings.change_reponame_prompt=Este mudanças vai afetar os links para este repositório.
settings.transfer=Transferir Propriedade settings.transfer=Transferir Propriedade
settings.transfer_desc=Transferir este repositório para outro usuário ou para uma organização onde você tem direitos de administrador. settings.transfer_desc=Transferir este repositório para outro usuário ou para uma organização onde você tem direitos de administrador.
settings.new_owner_has_same_repo=O novo dono já tem um repositório com o mesmo nome. settings.new_owner_has_same_repo=O novo dono já tem um repositório com o mesmo nome. Por favor, escolha outro nome.
settings.delete=Deletar Este Repositório settings.delete=Deletar Este Repositório
settings.delete_desc=Uma vez que você deleta um repositório, não tem volta. Por favor, tenha certeza. settings.delete_desc=Uma vez que você deleta um repositório, não tem volta. Por favor, tenha certeza.
settings.transfer_notices_1=- You will lose access if new owner is a individual user. settings.transfer_notices_1=- Você vai perder acesso se o novo dono for um usuário individual.
settings.transfer_notices_2=- You will conserve access if new owner is an organization and if you're one of the owners. settings.transfer_notices_2=- Você vai continuar tendo acesso se o novo dono é uma organização e você é um dos membros.
settings.transfer_form_title=Informe a seguinte informação para confirmar a sua operação: settings.transfer_form_title=Informe a seguinte informação para confirmar a sua operação:
settings.delete_notices_1=-Esta operação <strong>NÃO PODERÁ</strong> ser desfeita. settings.delete_notices_1=-Esta operação <strong>NÃO PODERÁ</strong> ser desfeita.
settings.delete_notices_2=- Esta operação irá apagar permanentemente o tudo deste repositório, incluindo os dados do Git, problemas, comentários e acessos dos colaboradores. settings.delete_notices_2=- Esta operação irá apagar permanentemente o tudo deste repositório, incluindo os dados do Git, problemas, comentários e acessos dos colaboradores.
settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion. settings.delete_notices_fork_1=- Se este repositório é público, todos os forks se tornarão independentes após a deleção.
settings.delete_notices_fork_2=- If this repository is private, all forks will be removed at the same time. settings.delete_notices_fork_2=- Se este repositório é privado, todos os forks serão removidos imediatamente.
settings.delete_notices_fork_3=- If you want to keep all forks after deletion, please change visibility of this repository to public first. settings.delete_notices_fork_3=- Se você deseja manter todos os forks, por favor muda a visibilidade do repositório para pública primeiro.
settings.update_settings_success=As opções do repositório foram atualizadas com sucesso. settings.update_settings_success=As opções do repositório foram atualizadas com sucesso.
settings.transfer_owner=Novo Dono settings.transfer_owner=Novo Dono
settings.make_transfer=Fazer Transferência settings.make_transfer=Fazer Transferência
@ -561,9 +565,9 @@ settings.remove_collaborator_success=O colaborador foi removido.
settings.user_is_org_member=O usuário é um membro da organização que não pode ser adicionado como um colaborador. settings.user_is_org_member=O usuário é um membro da organização que não pode ser adicionado como um colaborador.
settings.add_webhook=Adicionar Webhook settings.add_webhook=Adicionar Webhook
settings.hooks_desc=Hooks da web ou Webhooks permitem serviços externos serem notificados quando certos eventos acontecem no Gogs. Quando acontecem os eventos especificados, enviaremos uma solicitação POST para cada uma das URLs que você fornecer. Saiba mais no nosso <a target="_blank" href="%s"> Guia de Webhooks</a>. settings.hooks_desc=Hooks da web ou Webhooks permitem serviços externos serem notificados quando certos eventos acontecem no Gogs. Quando acontecem os eventos especificados, enviaremos uma solicitação POST para cada uma das URLs que você fornecer. Saiba mais no nosso <a target="_blank" href="%s"> Guia de Webhooks</a>.
settings.webhook_deletion=Delete Webhook settings.webhook_deletion=Deletar Webhook
settings.webhook_deletion_desc=Delete this webhook will remove its information and all delivery history. Do you want to continue? settings.webhook_deletion_desc=Deletar este Webhook vai remover sua informação e todo o histórico de entrega. Deseja continuar?
settings.webhook_deletion_success=Webhook has been deleted successfully! settings.webhook_deletion_success=Webhook deletado com sucesso!
settings.webhook.request=Solicitação settings.webhook.request=Solicitação
settings.webhook.response=Resposta settings.webhook.response=Resposta
settings.webhook.headers=Cabeçalhos settings.webhook.headers=Cabeçalhos
@ -578,10 +582,10 @@ settings.add_webhook_desc=Enviaremos uma solicitação <code>POST</code> para o
settings.payload_url=URL de carga settings.payload_url=URL de carga
settings.content_type=Tipo de Conteúdo settings.content_type=Tipo de Conteúdo
settings.secret=Secreto settings.secret=Secreto
settings.slack_username=Usuário settings.slack_username=Nome de usuário
settings.slack_icon_url=URL do ícone settings.slack_icon_url=URL do ícone
settings.slack_color=Cor settings.slack_color=Cor
settings.event_desc=Quais eventos você gostaria de acionar a esse hook da web? settings.event_desc=Quais eventos você gostaria de acionar a esse webhook?
settings.event_push_only=Apenas o evento <code>push</code>. settings.event_push_only=Apenas o evento <code>push</code>.
settings.event_send_everything=Preciso de <strong>tudo</strong>. settings.event_send_everything=Preciso de <strong>tudo</strong>.
settings.event_choose=Deixe-me escolher o que eu preciso. settings.event_choose=Deixe-me escolher o que eu preciso.
@ -592,9 +596,9 @@ settings.event_push_desc=Git push para o repositório
settings.active=Ativar settings.active=Ativar
settings.active_helper=Enviaremos detalhes do evento quando este hook for acionado. settings.active_helper=Enviaremos detalhes do evento quando este hook for acionado.
settings.add_hook_success=Novos hooks de web foram adicionados. settings.add_hook_success=Novos hooks de web foram adicionados.
settings.update_webhook=Atualizar Hook da Web settings.update_webhook=Atualizar Webhook
settings.update_hook_success=Hook da web atualizado. settings.update_hook_success=Webhook atualizado.
settings.delete_webhook=Excluir Hook da Web settings.delete_webhook=Excluir Webhook
settings.recent_deliveries=Entregas Recentes settings.recent_deliveries=Entregas Recentes
settings.hook_type=Tipo de Hook settings.hook_type=Tipo de Hook
settings.add_slack_hook_desc=Adicionar <a href="%s">Slack</a> de integração para o seu repositório. settings.add_slack_hook_desc=Adicionar <a href="%s">Slack</a> de integração para o seu repositório.
@ -613,23 +617,23 @@ settings.deploy_key_deletion=Exclusão de chave de deploy
settings.deploy_key_deletion_desc=Excluir esta chave de implantação removerá permissões de acesso a este repositório. Quer mesmo continuar? settings.deploy_key_deletion_desc=Excluir esta chave de implantação removerá permissões de acesso a este repositório. Quer mesmo continuar?
settings.deploy_key_deletion_success=Chave de implantação excluída com sucesso! settings.deploy_key_deletion_success=Chave de implantação excluída com sucesso!
diff.browse_source=Ver Fontes diff.browse_source=Ver Código Fonte
diff.parent=pai diff.parent=pai
diff.commit=commit diff.commit=commit
diff.data_not_available=Dados Diff não disponíveis. diff.data_not_available=Dados de Diff não disponíveis.
diff.show_diff_stats=Mostrar estatísticas do Diff diff.show_diff_stats=Mostrar estatísticas do Diff
diff.stats_desc=<strong> %d arquivos alterados</strong> com <strong>%d adições</strong> e <strong>%d exclusões</strong> diff.stats_desc=<strong> %d arquivos alterados</strong> com <strong>%d adições</strong> e <strong>%d exclusões</strong>
diff.bin=BIN diff.bin=BIN
diff.view_file=Ver Arquivo diff.view_file=Ver Arquivo
release.releases=Lançamentos release.releases=Versões
release.new_release=Novo Lançamento release.new_release=Nova Versão
release.draft=Rascunho release.draft=Rascunho
release.prerelease=Pré-Lançamento release.prerelease=Versão Prévia
release.stable=Estável release.stable=Estável
release.edit=editar release.edit=editar
release.ahead=<strong>%d</strong> commits para %s depois desta versão release.ahead=<strong>%d</strong> commits para %s depois desta versão
release.source_code=Código-fonte release.source_code=Código fonte
release.tag_name=Nome da tag release.tag_name=Nome da tag
release.target=Destino release.target=Destino
release.tag_helper=Escolha uma tag existente, ou crie uma nova tag em publicar. release.tag_helper=Escolha uma tag existente, ou crie uma nova tag em publicar.
@ -648,6 +652,7 @@ release.tag_name_already_exist=Já existiu versão com esse nome de tag.
[org] [org]
org_name_holder=Nome da Organização org_name_holder=Nome da Organização
org_full_name_holder=Nome completo da organização
org_name_helper=Nomes de grandes organizações são curtos e memoráveis. org_name_helper=Nomes de grandes organizações são curtos e memoráveis.
create_org=Criar Organização create_org=Criar Organização
repo_updated=Atualizado repo_updated=Atualizado
@ -674,15 +679,15 @@ settings.website=Site
settings.location=Localização settings.location=Localização
settings.update_settings=Atualizar Configurações settings.update_settings=Atualizar Configurações
settings.update_setting_success=Configuração da organização atualizada com sucesso. settings.update_setting_success=Configuração da organização atualizada com sucesso.
settings.change_orgname_prompt=This change will affect how links relate to the organization. settings.change_orgname_prompt=Esta mudança vai afetar os links para esta organização.
settings.update_avatar_success=Organization avatar setting has been updated successfully. settings.update_avatar_success=A configuração de avatar da organização foi atualizado com sucesso.
settings.delete=Deletar Organização settings.delete=Deletar Organização
settings.delete_account=Deletar Esta Organização settings.delete_account=Deletar Esta Organização
settings.delete_prompt=A operação deletará esta organização permanentemente, e <strong>NÃO PODERÁ</strong> ser desfeita! settings.delete_prompt=A operação deletará esta organização permanentemente, e <strong>NÃO PODERÁ</strong> ser desfeita!
settings.confirm_delete_account=Confirmar Deleção settings.confirm_delete_account=Confirmar Deleção
settings.delete_org_title=Deleção da Organização settings.delete_org_title=Deleção da Organização
settings.delete_org_desc=Esta organização será deletada permanentemente, você quer continuar? settings.delete_org_desc=Esta organização será deletada permanentemente, você quer continuar?
settings.hooks_desc=Adicionar Hooks da Web que serão acionados para <strong>todos os repositórios</strong> dessa organização. settings.hooks_desc=Adicionar Webhooks que serão acionados para <strong>todos os repositórios</strong> dessa organização.
members.public=Público members.public=Público
members.public_helper=tornar privado members.public_helper=tornar privado
@ -790,25 +795,26 @@ users.new_account=Criar Nova Conta
users.name=Nome users.name=Nome
users.activated=Ativado users.activated=Ativado
users.admin=Administrador users.admin=Administrador
users.repos=Repos users.repos=Repositórios
users.created=Criado users.created=Criado
users.send_register_notify=Send Registration Notification To User users.send_register_notify=Enviar notificação de registro para ao usuário
users.new_success=Nova conta '%s' foi criada com sucesso. users.new_success=Nova conta '%s' foi criada com sucesso.
users.edit=Editar users.edit=Editar
users.auth_source=Fonte da autenticação users.auth_source=Fonte da autenticação
users.local=Local users.local=Local
users.auth_login_name=Nome de login da autenticação users.auth_login_name=Nome de login da autenticação
users.password_helper=Leave it empty to remain unchanged. users.password_helper=Deixe em branco para não mudar.
users.update_profile_success=O perfil da conta foi atualizado com sucesso. users.update_profile_success=O perfil da conta foi atualizado com sucesso.
users.edit_account=Editar Conta users.edit_account=Editar Conta
users.is_activated=Esta conta está ativada users.is_activated=Esta conta está ativada
users.is_admin=Esta conta tem permissões de administrador users.is_admin=Esta conta tem permissões de administrador
users.allow_git_hook=Esta conta tem permissões para criar ganchos Git users.allow_git_hook=Esta conta tem permissões para criar hooks do Git
users.allow_import_local=Esta conta tem permissões para importar repositórios locais
users.update_profile=Atualizar Perfil da Conta users.update_profile=Atualizar Perfil da Conta
users.delete_account=Deletar Esta Conta users.delete_account=Deletar Esta Conta
users.still_own_repo=Sua conta ainda é proprietária do repositório, você tem que excluir ou transferi-lo primeiro. users.still_own_repo=Sua conta ainda é proprietária do repositório, você tem que excluir ou transferi-lo primeiro.
users.still_has_org=Sua conta ainda faz parte da organização, você deve sair ou excluí-la primeiro. users.still_has_org=Sua conta ainda faz parte da organização, você deve sair ou excluí-la primeiro.
users.deletion_success=Account has been deleted successfully! users.deletion_success=Conta deletada com sucesso!
orgs.org_manage_panel=Painel de Gerenciamento da Organização orgs.org_manage_panel=Painel de Gerenciamento da Organização
orgs.name=Nome orgs.name=Nome
@ -820,7 +826,7 @@ repos.owner=Dono
repos.name=Nome repos.name=Nome
repos.private=Privado repos.private=Privado
repos.watches=Observadores repos.watches=Observadores
repos.stars=Estrelas repos.stars=Favoritos
repos.issues=Problemas repos.issues=Problemas
auths.auth_manage_panel=Painel de gerenciamento da autenticação auths.auth_manage_panel=Painel de gerenciamento da autenticação
@ -834,16 +840,16 @@ auths.auth_name=Nome da autenticação
auths.domain=Domínio auths.domain=Domínio
auths.host=Host auths.host=Host
auths.port=Porta auths.port=Porta
auths.bind_dn=Bind DN auths.bind_dn=Vincular DN
auths.bind_password=Bind Password auths.bind_password=Vincular senha
auths.bind_password_helper=Atenção: Esta senha é armazenada em texto plano. Não use uma conta com muitos privilégios. auths.bind_password_helper=Atenção: Esta senha é armazenada em texto plano. Não use uma conta com muitos privilégios.
auths.user_base=Base de pesquisa do usuário auths.user_base=Base de pesquisa do usuário
auths.user_dn=User DN auths.user_dn=Usuário do DN
auths.attribute_name=Atributo primeiro nome auths.attribute_name=Atributo primeiro nome
auths.attribute_surname=Atributo sobrenome auths.attribute_surname=Atributo sobrenome
auths.attribute_mail=Atributo e-mail auths.attribute_mail=Atributo e-mail
auths.filter=User Filter auths.filter=Filtro de usuário
auths.admin_filter=Admin Filter auths.admin_filter=Filtro de administrador
auths.ms_ad_sa=Ms Ad SA auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=Tipo de autenticação SMTP auths.smtp_auth=Tipo de autenticação SMTP
auths.smtphost=Host SMTP auths.smtphost=Host SMTP
@ -857,13 +863,13 @@ auths.enable_auto_register=Habilitar Registro Automático
auths.tips=Dicas auths.tips=Dicas
auths.edit=Editar a configuração de autenticação auths.edit=Editar a configuração de autenticação
auths.activated=Esta autenticação foi ativada auths.activated=Esta autenticação foi ativada
auths.new_success=New authentication '%s' has been added successfully. auths.new_success=Nova autenticação '%s' foi adicionada com sucesso.
auths.update_success=A configuração da autenticação foi atualizada com sucesso. auths.update_success=A configuração da autenticação foi atualizada com sucesso.
auths.update=Atualizar a configuração da autenticação auths.update=Atualizar a configuração da autenticação
auths.delete=Excluir esta autenticação auths.delete=Excluir esta autenticação
auths.delete_auth_title=Exclusão da autenticação auths.delete_auth_title=Exclusão da autenticação
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue? auths.delete_auth_desc=Esta autenticação esta prestes a ser deletada, deseja continuar?
auths.deletion_success=Authentication has been deleted successfully! auths.deletion_success=Autenticação deletada com sucesso!
config.server_config=Configuração do Servidor config.server_config=Configuração do Servidor
config.app_name=Nome do Aplicativo config.app_name=Nome do Aplicativo
@ -895,7 +901,7 @@ config.show_registration_button=Mostrar Botão de Registo
config.require_sign_in_view=Requerer Entrar no Gogs para Ver config.require_sign_in_view=Requerer Entrar no Gogs para Ver
config.enable_cache_avatar=Habilitar Cache de Avatar config.enable_cache_avatar=Habilitar Cache de Avatar
config.mail_notify=Notificação de Correio config.mail_notify=Notificação de Correio
config.disable_key_size_check=Disable Minimum Key Size Check config.disable_key_size_check=Desativar verificação de tamanho mínimo da chave
config.enable_captcha=Habilitar o Captcha config.enable_captcha=Habilitar o Captcha
config.active_code_lives=Ativar Code Lives config.active_code_lives=Ativar Code Lives
config.reset_password_code_lives=Redefinir Senha de Code Lives config.reset_password_code_lives=Redefinir Senha de Code Lives
@ -950,12 +956,12 @@ notices.delete_success=Aviso do sistema foi deletado com sucesso.
[action] [action]
create_repo=repositório criado <a href="%s"> %s</a> create_repo=repositório criado <a href="%s"> %s</a>
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a> rename_repo=renomeou o o repositório <code>%[1]s</code> para <a href="%[2]s">%[3]s</a>
commit_repo=pushed para <a href="%s/src/%s">%[2]s</a> em <a href="%[1]s">%[3]s</a> commit_repo=pushed para <a href="%s/src/%s">%[2]s</a> em <a href="%[1]s">%[3]s</a>
create_issue='questão aberta <a href="%s/issues/%s">%s#%[2]s</a>' create_issue='questão aberta <a href="%s/issues/%s">%s#%[2]s</a>'
create_pull_request=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>` create_pull_request=`criou o pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
comment_issue='comentou sobre a questão <a href="%s/issues/%s">%s#%[2]s</a>' comment_issue='comentou sobre a questão <a href="%s/issues/%s">%s#%[2]s</a>'
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>` merge_pull_request=`mesclou o pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
transfer_repo=repositório transferido de <code>%s</code> para <a href="%s">%s</a> transfer_repo=repositório transferido de <code>%s</code> para <a href="%s">%s</a>
push_tag=Foi feito push na tag <a href="%s/src/%s">%[2]s</a> para <a href="%[1]s">%[3]s</a> push_tag=Foi feito push na tag <a href="%s/src/%s">%[2]s</a> para <a href="%[1]s">%[3]s</a>
compare_2_commits=Ver comparação desses 2 commits compare_2_commits=Ver comparação desses 2 commits

488
conf/locale/locale_ru-RU.ini

@ -99,7 +99,7 @@ server_service_title=Сервер и другие настройки служб
offline_mode=Включение офлайн режима offline_mode=Включение офлайн режима
offline_mode_popup=Отключить CDN даже в производственном режиме, все файлы ресурсов будут раздаваться локально. offline_mode_popup=Отключить CDN даже в производственном режиме, все файлы ресурсов будут раздаваться локально.
disable_gravatar=Отключить службу Gravatar disable_gravatar=Отключить службу Gravatar
disable_gravatar_popup=Disable Gravatar and custom sources, all avatars are uploaded by users or default. disable_gravatar_popup=Отключить Gravatar и пользовательские источники, все аватары по-умолчанию загружаются пользователями.
disable_registration=Отключить самостоятельную регистрацию disable_registration=Отключить самостоятельную регистрацию
disable_registration_popup=Запретить пользователям самостоятельную регистрацию, только администратор может создавать аккаунты. disable_registration_popup=Запретить пользователям самостоятельную регистрацию, только администратор может создавать аккаунты.
enable_captcha=Включить капчу enable_captcha=Включить капчу
@ -111,7 +111,7 @@ admin_title=Настройки учётной записи администра
admin_name=Имя пользователя admin_name=Имя пользователя
admin_password=Пароль admin_password=Пароль
confirm_password=Подтвердить пароль confirm_password=Подтвердить пароль
admin_email=Эл. почта admin_email=Электронная почта администратора
install_gogs=Установить Gogs install_gogs=Установить Gogs
test_git_failed=Не удалось проверить 'git' команду: %v test_git_failed=Не удалось проверить 'git' команду: %v
sqlite3_not_available=Ваша версия не поддерживает SQLite3, пожалуйста скачайте официальную бинарную версию от %s, а не версию gobuild. sqlite3_not_available=Ваша версия не поддерживает SQLite3, пожалуйста скачайте официальную бинарную версию от %s, а не версию gobuild.
@ -148,7 +148,6 @@ forgot_password=Забыли пароль
forget_password=Забыли пароль? forget_password=Забыли пароль?
sign_up_now=Нужен аккаунт? Зарегистрируйтесь. sign_up_now=Нужен аккаунт? Зарегистрируйтесь.
confirmation_mail_sent_prompt=Новое письмо для подтверждения было направлено на <b>%s</b>, пожалуйста, проверьте ваш почтовый ящик в течение %d часов для завершения регистрации. confirmation_mail_sent_prompt=Новое письмо для подтверждения было направлено на <b>%s</b>, пожалуйста, проверьте ваш почтовый ящик в течение %d часов для завершения регистрации.
sign_in_to_account=Войдите свой аккаунт
active_your_account=Активируйте свой аккаунт active_your_account=Активируйте свой аккаунт
resent_limit_prompt=Вы слишком часто отправляете письмо с активацией. Подождите 3 минуты, пожалуйста. resent_limit_prompt=Вы слишком часто отправляете письмо с активацией. Подождите 3 минуты, пожалуйста.
has_unconfirmed_mail=Здравствуйте, %s! У вас есть неподтвержденный адрес электронной почты (<b>%s</b>). Если вам не приходило письмо с подтверждением или нужно выслать новое письмо, нажмите на кнопку ниже. has_unconfirmed_mail=Здравствуйте, %s! У вас есть неподтвержденный адрес электронной почты (<b>%s</b>). Если вам не приходило письмо с подтверждением или нужно выслать новое письмо, нажмите на кнопку ниже.
@ -192,6 +191,7 @@ min_size_error=«должен содержать по крайней мере %s
max_size_error=` должен содержать максимум %s символов.` max_size_error=` должен содержать максимум %s символов.`
email_error=«не является адресом электронной почты.» email_error=«не является адресом электронной почты.»
url_error=«не является допустимым URL-адресом.» url_error=«не является допустимым URL-адресом.»
include_error=` должен содержать '%s'`
unknown_error=Неизвестная ошибка: unknown_error=Неизвестная ошибка:
captcha_incorrect=CAPTCHA не совпадает. captcha_incorrect=CAPTCHA не совпадает.
password_not_match=Пароль и подтверждение пароля не совпадают. password_not_match=Пароль и подтверждение пароля не совпадают.
@ -318,9 +318,9 @@ token_name=Имя маркера
generate_token=Генерировать маркер generate_token=Генерировать маркер
generate_token_succees=Успешно создан новый токен доступа! Пожалуйста сделайте копию вашего нового токена персонального доступа. Вы не сможете увидеть его снова! generate_token_succees=Успешно создан новый токен доступа! Пожалуйста сделайте копию вашего нового токена персонального доступа. Вы не сможете увидеть его снова!
delete_token=Удалить delete_token=Удалить
access_token_deletion=Personal Access Token Deletion access_token_deletion=Удаление персонального токена доступа
access_token_deletion_desc=Delete this personal access token will remove all related accesses of application. Do you want to continue? access_token_deletion_desc=Удаление этого персонального токена доступа приведет к удалению всех связанных прав доступа к приложению. Вы хотите продолжить?
delete_token_success=Personal access token has been removed successfully! Don't forget to update your application as well. delete_token_success=Персональный токен доступа успешно удален! Не забудьте изменить настройки вашего приложения.
delete_account=Удалить свой аккаунт delete_account=Удалить свой аккаунт
delete_prompt=Этим действием вы удалите свою учетную запись навсегда и <strong>НЕ СМОЖЕТЕ</strong> ее вернуть! delete_prompt=Этим действием вы удалите свою учетную запись навсегда и <strong>НЕ СМОЖЕТЕ</strong> ее вернуть!
@ -333,8 +333,9 @@ owner=Владелец
repo_name=Имя репозитория repo_name=Имя репозитория
repo_name_helper=Лучшие названия репозиториев коротки, запоминаемы и <strong>уникальны</strong>. repo_name_helper=Лучшие названия репозиториев коротки, запоминаемы и <strong>уникальны</strong>.
visibility=Видимость visibility=Видимость
visiblity_helper=This repository is <span class="ui red text">Private</span> visiblity_helper=<span class="ui red text">Личный</span> репозиторий
visiblity_fork_helper=(Change of this value will affect all forks) visiblity_helper_forced=Все новые репозитории являются <span class="ui red text">Личными</span> по желанию администратора сайта
visiblity_fork_helper=(Изменение этого значения затронет все форки)
fork_repo=Ответвить репозиторий fork_repo=Ответвить репозиторий
fork_from=Ответвление от fork_from=Ответвление от
fork_visiblity_helper=Ответвленному репозиторию нельзя поменять уровень видимости fork_visiblity_helper=Ответвленному репозиторию нельзя поменять уровень видимости
@ -355,17 +356,18 @@ form.name_pattern_not_allowed=Шаблон имени репозитория '%s
need_auth=Требуется авторизация need_auth=Требуется авторизация
migrate_type=Тип миграции migrate_type=Тип миграции
migrate_type_helper=This repository will be a <span class="text blue">mirror</span> migrate_type_helper=Этот репозиторий будет <span class="text blue">зеркалом</span>
migrate_repo=Перенос репозитория migrate_repo=Перенос репозитория
migrate.clone_address=Скопировать адрес migrate.clone_address=Скопировать адрес
migrate.clone_address_desc=This can be a HTTP/HTTPS/GIT URL or local server path. migrate.clone_address_desc=Это может быть HTTP/HTTPS/GIT адрес или локальный путь на сервере.
migrate.permission_denied=У вас нет прав на импорт локальных репозиториев.
migrate.invalid_local_path=Недопустимый локальный путь. Возможно он не существует или является не папкой. migrate.invalid_local_path=Недопустимый локальный путь. Возможно он не существует или является не папкой.
forked_from=forked from forked_from=форк от
fork_from_self=Вы не можете форкнуть репозитарий, так как Вы уже его владелец! fork_from_self=Вы не можете форкнуть репозитарий, так как Вы уже его владелец!
copy_link=Скопировать copy_link=Скопировать
copy_link_success=Скопировано! copy_link_success=Скопировано!
copy_link_error=Press ⌘-C or Ctrl-C to copy copy_link_error=Нажмите ⌘-C или Ctrl-C для копирования
click_to_copy=Скопировать в буфер обмена click_to_copy=Скопировать в буфер обмена
copied=Успешно скопировано copied=Успешно скопировано
clone_helper=Нужна помощь в клонировании? Посетите страницу <a target="_blank" href="%s">помощи</a>! clone_helper=Нужна помощь в клонировании? Посетите страницу <a target="_blank" href="%s">помощи</a>!
@ -380,7 +382,7 @@ quick_guide=Краткое руководство
clone_this_repo=Клонировать репозиторий clone_this_repo=Клонировать репозиторий
create_new_repo_command=Создать новый репозиторий из командной строки create_new_repo_command=Создать новый репозиторий из командной строки
push_exist_repo=Отправить существующий репозиторий из командной строки push_exist_repo=Отправить существующий репозиторий из командной строки
repo_is_empty=This repository is empty, please come back later! repo_is_empty=Этот репозиторий пуст, пожалуйста, возвращайтесь позже!
branch=Ветка branch=Ветка
@ -389,7 +391,7 @@ branch_and_tags=Ветки и метки
branches=Ветки branches=Ветки
tags=Метки tags=Метки
issues=Обсуждения issues=Обсуждения
pulls=Pull Requests pulls=Пулл реквесты
labels=Метки labels=Метки
milestones=Этапы milestones=Этапы
commits=Коммиты commits=Коммиты
@ -412,26 +414,26 @@ issues.new=Новая задача
issues.new.labels=Метки issues.new.labels=Метки
issues.new.no_label=Не метка issues.new.no_label=Не метка
issues.new.clear_labels=Отчистить метки issues.new.clear_labels=Отчистить метки
issues.new.milestone=Milestone issues.new.milestone=Этап
issues.new.no_milestone=No Milestone issues.new.no_milestone=Нет этапа
issues.new.clear_milestone=Clear milestone issues.new.clear_milestone=Очистить этап
issues.new.open_milestone=Open Milestones issues.new.open_milestone=Открыть этап
issues.new.closed_milestone=Closed Milestones issues.new.closed_milestone=Завершенные этапы
issues.new.assignee=Assignee issues.new.assignee=Ответственный
issues.new.clear_assignee=Clear assignee issues.new.clear_assignee=Убрать ответственного
issues.new.no_assignee=No assignee issues.new.no_assignee=Нет ответственного
issues.create=Create Issue issues.create=Добавить задачу
issues.new_label=Новая метка issues.new_label=Новая метка
issues.new_label_placeholder=Имя метки... issues.new_label_placeholder=Имя метки...
issues.create_label=Create Label issues.create_label=Добавить метку
issues.open_tab=%d Открыть issues.open_tab=%d Открыть
issues.close_tab=%d Закрыть issues.close_tab=%d Закрыть
issues.filter_label=Метка issues.filter_label=Метка
issues.filter_label_no_select=Нет выбранной метки issues.filter_label_no_select=Нет выбранной метки
issues.filter_milestone=Этап issues.filter_milestone=Этап
issues.filter_milestone_no_select=No selected milestone issues.filter_milestone_no_select=Этап не выбран
issues.filter_assignee=Назначено issues.filter_assignee=Назначено
issues.filter_assginee_no_select=No selected Assignee issues.filter_assginee_no_select=Ответственный не выбран
issues.filter_type=Тип issues.filter_type=Тип
issues.filter_type.all_issues=Все задачи issues.filter_type.all_issues=Все задачи
issues.filter_type.assigned_to_you=Назначено Вам issues.filter_type.assigned_to_you=Назначено Вам
@ -440,35 +442,35 @@ issues.filter_type.mentioning_you=Вы упомянуты
issues.filter_sort=Сортировать issues.filter_sort=Сортировать
issues.filter_sort.latest=Новейшие issues.filter_sort.latest=Новейшие
issues.filter_sort.oldest=Старейшие issues.filter_sort.oldest=Старейшие
issues.filter_sort.recentupdate=Recently updated issues.filter_sort.recentupdate=Недавно обновленные
issues.filter_sort.leastupdate=Least recently updated issues.filter_sort.leastupdate=Давно обновленные
issues.filter_sort.mostcomment=Most commented issues.filter_sort.mostcomment=Большего комментариев
issues.filter_sort.leastcomment=Least commented issues.filter_sort.leastcomment=Меньше комментариев
issues.opened_by=opened %[1]s by <a href="%[2]s">%[3]s</a> issues.opened_by=%[1] открыта <a href="%[2]s">%[3]s</a>
issues.opened_by_fake=opened %[1]s by %[2]s issues.opened_by_fake=%[1]s открыта %[2]s
issues.previous=Предыдущая страница issues.previous=Предыдущая страница
issues.next=Следующая страница issues.next=Следующая страница
issues.open_title=Open issues.open_title=Открыта
issues.closed_title=Closed issues.closed_title=Закрыта
issues.num_comments=%d comments issues.num_comments=комментариев: %d
issues.commented_at=`commented <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commented_at=` прокомментировал <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.no_content=There is no content yet. issues.no_content=Пока нет содержимого.
issues.close_issue=Close issues.close_issue=Закрыть
issues.close_comment_issue=Close and comment issues.close_comment_issue=Прокомментировать и закрыть
issues.reopen_issue=Reopen issues.reopen_issue=Открыть снова
issues.reopen_comment_issue=Reopen and comment issues.reopen_comment_issue=Прокомментировать и открыть
issues.create_comment=Comment issues.create_comment=Комментировать
issues.closed_at=`closed <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at=`закрыл <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`reopened <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`открыл снова <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at=`referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at=`упомянул эту задачу в коммите <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.poster=Poster issues.poster=Автор
issues.admin=Администратор issues.admin=Администратор
issues.owner=Владелец issues.owner=Владелец
issues.sign_up_for_free=Зарегистрируйтесь бесплатно issues.sign_up_for_free=Зарегистрируйтесь бесплатно
issues.sign_in_require_desc=to join this conversation. Already have an account? <a href="%s">Sign in to comment</a> issues.sign_in_require_desc=чтобы присоединиться к обсуждению. Уже есть аккаунт? <a href="%s">Войдите чтобы прокомментировать</a>
issues.edit=Изменить issues.edit=Изменить
issues.cancel=Cancel issues.cancel=Отмена
issues.save=Save issues.save=Сохранить
issues.label_title=Имя метки issues.label_title=Имя метки
issues.label_color=Цвет метки issues.label_color=Цвет метки
issues.label_count=%d меток issues.label_count=%d меток
@ -480,52 +482,54 @@ issues.label_deletion=Удаление метки
issues.label_deletion_desc=Удаление ярлыка затронет все связанные задачи. Продолжить? issues.label_deletion_desc=Удаление ярлыка затронет все связанные задачи. Продолжить?
issues.label_deletion_success=Метка была удалена успешно! issues.label_deletion_success=Метка была удалена успешно!
pulls.compare_changes=Compare Changes pulls.compare_changes=Сравнить изменения
pulls.compare_changes_desc=Compare two branches and make a pull request for changes. pulls.compare_changes_desc=Сравнить две ветки и создать пулл реквест для изменений.
pulls.compare_base=base pulls.compare_base=родительская ветка
pulls.compare_compare=compare pulls.compare_compare=сравнить
pulls.filter_branch=Filter branch pulls.filter_branch=Фильтр по ветке
pulls.no_results=No results found. pulls.no_results=Результатов не найдено.
pulls.nothing_to_compare=There is nothing to compare because base and head branches are even. pulls.nothing_to_compare=Нечего сравнивать, родительская и текущая ветка одинаковые.
pulls.has_pull_request=`There is already a pull request between these two targets: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>` pulls.has_pull_request=`Уже существует пулл-реквест между двумя целями <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>`
pulls.create=Create Pull Request pulls.create=Создать пулл-реквест
pulls.title_desc=wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> pulls.title_desc=хочет смерджить %[1]d коммит(ов) из <code>%[2]s</code> в <code>%[3]s</code>
pulls.merged_title_desc=merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s pulls.merged_title_desc=слито %[1]d коммит(ов) из <code>%[2]s</code> в <code>%[3]s</code> %[4]s
pulls.tab_conversation=Обсуждение pulls.tab_conversation=Обсуждение
pulls.tab_commits=Коммиты pulls.tab_commits=Коммиты
pulls.tab_files=Измененные файлы pulls.tab_files=Измененные файлы
pulls.reopen_to_merge=Please reopen this pull request to perform merge operation. pulls.reopen_to_merge=Пожалуйста пересоздайте пулл-реквест для слияния.
pulls.merged=Merged pulls.merged=Слито
pulls.has_merged=This pull request has been merged successfully! pulls.has_merged=Слияние этого пулл-реквеста успешно завершено!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information. pulls.data_broken=Содержимое этого пулл-реквеста было нарушено, вследствии удаления или клонирования информации.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request. pulls.is_checking=Продолжается проверка конфликтов, пожалуйста обновите страницу несколько позже.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits. pulls.can_auto_merge_desc=Вы можете провести операцию автоматического слияния для этого пулл-реквеста.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it. pulls.cannot_auto_merge_desc=Вы не можете произвести операцию автоматического слияния, потому как существуют конфликты между коммитами.
pulls.merge_pull_request=Merge Pull Request pulls.cannot_auto_merge_helper=Используйте командную строку для решения этого.
pulls.merge_pull_request=Слить пулл-реквест
milestones.new=New Milestone pulls.open_unmerged_pull_exists=`Вы не можете произвести операцию переоткрытия, потому что уже существует пулл-реквест (#%d) из этого же репозитория, с такими же параметрами слияния, который ожидает слияния.`
milestones.open_tab=%d Open
milestones.close_tab=%d Closed milestones.new=Новая контрольная точка
milestones.closed=Closed %s milestones.open_tab=%d открыты
milestones.no_due_date=No due date milestones.close_tab=%d Закрыт
milestones.open=Open milestones.closed=Закрыт %s
milestones.close=Close milestones.no_due_date=Срок не указан
milestones.new_subheader=Create milestones to organize your issues. milestones.open=Открыть
milestones.create=Create Milestone milestones.close=Закрыть
milestones.title=Title milestones.new_subheader=Создавайте контрольные точки для трекинга ваших вопросов.
milestones.desc=Description milestones.create=Создать контрольную точку
milestones.due_date=Due Date (optional) milestones.title=Заголовок
milestones.clear=Clear milestones.desc=Описание
milestones.invalid_due_date_format=Due date format is invalid, must be 'year-mm-dd'. milestones.due_date=Дата окончания (опционально)
milestones.create_success=Milestone '%s' has been created successfully! milestones.clear=Очистить
milestones.edit=Edit Milestone milestones.invalid_due_date_format=Некорректная дата окончания. Правильный формат - 'гггг-мм-дд'.
milestones.edit_subheader=Use better description for milestones so people won't be confused. milestones.create_success=Контрольная точка '%s' успешно создана!
milestones.cancel=Cancel milestones.edit=Изменить контрольную точку
milestones.modify=Modify Milestone milestones.edit_subheader=Используйте лучшее описание контрольной точки, во избежание непонимания со стороны других людей.
milestones.edit_success=Changes of milestone '%s' has been saved successfully! milestones.cancel=Отмена
milestones.deletion=Milestone Deletion milestones.modify=Изменить контрольную точку
milestones.deletion_desc=Delete this milestone will remove its information in all related issues. Do you want to continue? milestones.edit_success=Изменения контрольной точки '%s' успешно сохранены!
milestones.deletion_success=Milestone has been deleted successfully! milestones.deletion=Удаление контрольной точки
milestones.deletion_desc=Удаление этой контрольной точки приведет с удалению всей информации, во всех вопросах (Issues). Вы действительно хотите продолжить?
milestones.deletion_success=Контрольная точка успешно удалена!
settings=Настройки settings=Настройки
settings.options=Опции settings.options=Опции
@ -536,20 +540,20 @@ settings.basic_settings=Основные параметры
settings.danger_zone=Опасная зона settings.danger_zone=Опасная зона
settings.site=Официальный сайт settings.site=Официальный сайт
settings.update_settings=Обновить настройки settings.update_settings=Обновить настройки
settings.change_reponame_prompt=This change will affect how links relate to the repository. settings.change_reponame_prompt=Это изменение повлияет на отношения ссылок к этому репозиторию.
settings.transfer=Передать права собственности settings.transfer=Передать права собственности
settings.transfer_desc=Передать репозиторий другому пользователю или организации где у вас есть права администратора. settings.transfer_desc=Передать репозиторий другому пользователю или организации где у вас есть права администратора.
settings.new_owner_has_same_repo=У нового владельца уже есть хранилище с таким названием. settings.new_owner_has_same_repo=У нового владельца уже есть хранилище с таким названием.
settings.delete=Удалить этот репозиторий settings.delete=Удалить этот репозиторий
settings.delete_desc=Как только вы удалите репозиторий — пути назад не будет. Удостоверьтесь, что вам это точно нужно. settings.delete_desc=Как только вы удалите репозиторий — пути назад не будет. Удостоверьтесь, что вам это точно нужно.
settings.transfer_notices_1=- You will lose access if new owner is a individual user. settings.transfer_notices_1=- Вы можете потерять доступ, если новый владелец является отдельным пользователем.
settings.transfer_notices_2=- You will conserve access if new owner is an organization and if you're one of the owners. settings.transfer_notices_2=- Вы сохраните доступ, если новым владельцем станет организация, владельцем которой вы являетесь.
settings.transfer_form_title=Please enter following information to confirm your operation: settings.transfer_form_title=Введите сопутствующую информацию для подтверждения операции:
settings.delete_notices_1=- This operation <strong>CANNOT</strong> be undone. settings.delete_notices_1=- Эта операция <strong>НЕ МОЖЕТ</strong> быть отменена.
settings.delete_notices_2=- This operation will permanently delete the everything of this repository, including Git data, issues, comments and accesses of collaborators. settings.delete_notices_2=- Эта операция перманентно удалит всё из этого репозитория, включая данные Git, связанные с ним вопросы, комментарии и права доступа для сотрудников.
settings.delete_notices_fork_1=- If this repository is public, all forks will be became independent after deletion. settings.delete_notices_fork_1=- Если данный репозиторий является публичным, все склонированные репозитории останутся независимыми, после его удаления.
settings.delete_notices_fork_2=- If this repository is private, all forks will be removed at the same time. settings.delete_notices_fork_2=- Если данный репозиторий является приватным, все его форки будут удалены вместе с ним.
settings.delete_notices_fork_3=- If you want to keep all forks after deletion, please change visibility of this repository to public first. settings.delete_notices_fork_3=- Если вы хотите сохранить все форки после удаления репозитория, то сначала сделайте его публичным.
settings.update_settings_success=Настройка репозитория обновлена успешно. settings.update_settings_success=Настройка репозитория обновлена успешно.
settings.transfer_owner=Новый владелец settings.transfer_owner=Новый владелец
settings.make_transfer=Выполнить передачу settings.make_transfer=Выполнить передачу
@ -561,16 +565,16 @@ settings.remove_collaborator_success=Соавтор был удален.
settings.user_is_org_member=Пользователь является членом организации, члены которой не могут быть добавлены в качестве соавтора. settings.user_is_org_member=Пользователь является членом организации, члены которой не могут быть добавлены в качестве соавтора.
settings.add_webhook=Добавить Webhook settings.add_webhook=Добавить Webhook
settings.hooks_desc=Webhooks позволяют внешним службам получать уведомления при возникновении определенных событий на Gogs. При возникновении указанных событий мы отправим запрос POST на каждый заданный вами URL. Узнать больше можно в нашем <a target="_blank" href="%s">Руководстве по Webhooks</a>. settings.hooks_desc=Webhooks позволяют внешним службам получать уведомления при возникновении определенных событий на Gogs. При возникновении указанных событий мы отправим запрос POST на каждый заданный вами URL. Узнать больше можно в нашем <a target="_blank" href="%s">Руководстве по Webhooks</a>.
settings.webhook_deletion=Delete Webhook settings.webhook_deletion=Удалить веб-хук
settings.webhook_deletion_desc=Delete this webhook will remove its information and all delivery history. Do you want to continue? settings.webhook_deletion_desc=Удаление этого веб-хука приведет к удалению всей, связанной с ним, информации, включая историю. Хотите продолжить?
settings.webhook_deletion_success=Webhook has been deleted successfully! settings.webhook_deletion_success=Веб-хук успешно удален!
settings.webhook.request=Request settings.webhook.request=Запрос
settings.webhook.response=Response settings.webhook.response=Ответ
settings.webhook.headers=Headers settings.webhook.headers=Заголовки
settings.webhook.payload=Payload settings.webhook.payload=Содержимое запроса
settings.webhook.body=Body settings.webhook.body=Тело ответа
settings.githooks_desc=Git Hooks are powered by Git itself, you can edit files of supported hooks in the list below to perform custom operations. settings.githooks_desc=Git-хуки предоставляются Git самим по себе, вы можете изменять файлы поддерживаемых хуков из списка ниже чтобы выполнять внешние операции.
settings.githook_edit_desc=If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook. settings.githook_edit_desc=Если хук не активен, будет подставлен пример содержимого. Пустое значение в этом поле приведет к отключению хука.
settings.githook_name=Название Hook'a settings.githook_name=Название Hook'a
settings.githook_content=Перехватить содержание settings.githook_content=Перехватить содержание
settings.update_githook=Обновить Hook settings.update_githook=Обновить Hook
@ -578,19 +582,19 @@ settings.add_webhook_desc=Мы отправим запрос <code>POST</code>
settings.payload_url=URL обработчика settings.payload_url=URL обработчика
settings.content_type=Тип содержимого settings.content_type=Тип содержимого
settings.secret=Secret settings.secret=Secret
settings.slack_username=Username settings.slack_username=Имя пользователя
settings.slack_icon_url=Icon URL settings.slack_icon_url=URL иконки
settings.slack_color=Color settings.slack_color=Цвет
settings.event_desc=На какие события этот webhook должен срабатывать? settings.event_desc=На какие события этот webhook должен срабатывать?
settings.event_push_only=Просто <code>push</code> событие. settings.event_push_only=Просто <code>push</code> событие.
settings.event_send_everything=I need <strong>everything</strong>. settings.event_send_everything=Мне нужно <strong>все</strong>.
settings.event_choose=Let me choose what I need. settings.event_choose=Позвольте мне выбрать то, что нужно.
settings.event_create=Create settings.event_create=Создать
settings.event_create_desc=Branch, or tag created settings.event_create_desc=Ветка или тэг созданы
settings.event_push=Push settings.event_push=Push
settings.event_push_desc=Git push to a repository settings.event_push_desc=Push в репозиторий
settings.active=Активен settings.active=Активен
settings.active_helper=Details regarding the event which triggered the hook will be delivered as well. settings.active_helper=Подробности о событии, вызвавшем срабатывание хука, также будут предоставлены.
settings.add_hook_success=Был добавлен новый webhook. settings.add_hook_success=Был добавлен новый webhook.
settings.update_webhook=Обновление Webhook settings.update_webhook=Обновление Webhook
settings.update_hook_success=Webhook обновлен. settings.update_hook_success=Webhook обновлен.
@ -602,16 +606,16 @@ settings.slack_token=Token
settings.slack_domain=Домен settings.slack_domain=Домен
settings.slack_channel=Канал settings.slack_channel=Канал
settings.deploy_keys=Ключи развертывания settings.deploy_keys=Ключи развертывания
settings.add_deploy_key=Add Deploy Key settings.add_deploy_key=Добавить ключ развертывания
settings.no_deploy_keys=You haven't added any deploy key. settings.no_deploy_keys=Вы не добавляли ключи развертывания.
settings.title=Title settings.title=Заголовок
settings.deploy_key_content=Content settings.deploy_key_content=Содержимое
settings.key_been_used=Deploy key content has been used. settings.key_been_used=Содержимое ключа развертывания уже используется.
settings.key_name_used=Deploy key with same name has already existed. settings.key_name_used=Ключ развертывания с таким заголовком уже существует.
settings.add_key_success=New deploy key '%s' has been added successfully! settings.add_key_success=Новый ключ развертывания '%s' успешно добавлен!
settings.deploy_key_deletion=Delete Deploy Key settings.deploy_key_deletion=Удалить ключ развертывания
settings.deploy_key_deletion_desc=Delete this deploy key will remove all related accesses for this repository. Do you want to continue? settings.deploy_key_deletion_desc=Удаление ключа развертывания приведет к удалению всех связанных прав доступа к репозиторию. Вы хотите продолжить?
settings.deploy_key_deletion_success=Deploy key has been deleted successfully! settings.deploy_key_deletion_success=Ключ развертывания успешно удален!
diff.browse_source=Просмотр исходного кода diff.browse_source=Просмотр исходного кода
diff.parent=Родитель diff.parent=Родитель
@ -640,7 +644,7 @@ release.preview=Предварительный просмотр
release.content_placeholder=Напишите что-нибудь release.content_placeholder=Напишите что-нибудь
release.loading=Загрузка... release.loading=Загрузка...
release.prerelease_desc=Это предварительный релиз release.prerelease_desc=Это предварительный релиз
release.prerelease_helper=We’ll point out that this release is not production-ready. release.prerelease_helper=Отдельно отметим, что этот релиз не готов к использованию в продакшене.
release.publish=Опубликовать релиз release.publish=Опубликовать релиз
release.save_draft=Сохранить черновик release.save_draft=Сохранить черновик
release.edit_release=Редактировать релиз release.edit_release=Редактировать релиз
@ -648,6 +652,7 @@ release.tag_name_already_exist=Релиз с этим именем тега уж
[org] [org]
org_name_holder=Название организации org_name_holder=Название организации
org_full_name_holder=Полное название организации
org_name_helper=Лучшие названия организаций коротки и запоминаемы. org_name_helper=Лучшие названия организаций коротки и запоминаемы.
create_org=Создать Организацию create_org=Создать Организацию
repo_updated=Обновлено repo_updated=Обновлено
@ -661,7 +666,7 @@ org_desc=Описание
team_name=Название команды team_name=Название команды
team_desc=Описание team_desc=Описание
team_name_helper=Вы будете использовать это имя для упоминания этой команды в обсуждении. team_name_helper=Вы будете использовать это имя для упоминания этой команды в обсуждении.
team_desc_helper=What is this team all about? team_desc_helper=Что это за команда?
team_permission_desc=Какой уровень разрешений должен быть у этой команды? team_permission_desc=Какой уровень разрешений должен быть у этой команды?
form.name_reserved=Наименование организации '%s' зарезервированно. form.name_reserved=Наименование организации '%s' зарезервированно.
@ -674,8 +679,8 @@ settings.website=Сайт
settings.location=Местоположение settings.location=Местоположение
settings.update_settings=Обновить настройки settings.update_settings=Обновить настройки
settings.update_setting_success=Настройки Организации были успешно обновлены. settings.update_setting_success=Настройки Организации были успешно обновлены.
settings.change_orgname_prompt=This change will affect how links relate to the organization. settings.change_orgname_prompt=Это изменение затронет все связанные с организацией, ссылки.
settings.update_avatar_success=Organization avatar setting has been updated successfully. settings.update_avatar_success=Аватар организации успешно обновлен.
settings.delete=Удалить Организацию settings.delete=Удалить Организацию
settings.delete_account=Удалить Эту Организацию settings.delete_account=Удалить Эту Организацию
settings.delete_prompt=Это действие безвозвратно удалит эту организацию навсегда. settings.delete_prompt=Это действие безвозвратно удалит эту организацию навсегда.
@ -716,7 +721,7 @@ teams.delete_team_desc=Эта команда будет удалена. Вы х
teams.delete_team_success=Данная команда была удалена успешно. teams.delete_team_success=Данная команда была удалена успешно.
teams.read_permission_desc=Эта команда предоставляет доступ на <strong>Чтение</strong>: члены могут просматривать и клонировать репозитории команды. teams.read_permission_desc=Эта команда предоставляет доступ на <strong>Чтение</strong>: члены могут просматривать и клонировать репозитории команды.
teams.write_permission_desc=Эта команда предоставляет доступ на <strong>Запись</strong>: члены могут получать и выполнять push команды в репозитории. teams.write_permission_desc=Эта команда предоставляет доступ на <strong>Запись</strong>: члены могут получать и выполнять push команды в репозитории.
teams.admin_permission_desc=This team grants <strong>Admin</strong> access: members can read from, push to, and add collaborators to the team's repositories. teams.admin_permission_desc=Эта команда дает <strong>административный</strong> доступ: участники могут читать, пушить и добавлять соавторов к ее репозиториям.
teams.repositories=Репозитории группы разработки teams.repositories=Репозитории группы разработки
teams.add_team_repository=Добавить репозиторий группы разработки teams.add_team_repository=Добавить репозиторий группы разработки
teams.remove_repo=Удалить teams.remove_repo=Удалить
@ -731,9 +736,9 @@ authentication=Авторизация
config=Настройки config=Настройки
notices=Системные уведомления notices=Системные уведомления
monitor=Мониторинг monitor=Мониторинг
first_page=First first_page=Первый
last_page=Last last_page=Последний
total=Total: %d total=Всего: %d
dashboard.statistic=Статистика dashboard.statistic=Статистика
dashboard.operations=Операции dashboard.operations=Операции
@ -752,70 +757,71 @@ dashboard.git_gc_repos=Выполнить сборку мусора на реп
dashboard.git_gc_repos_success=Сборка мусора на всех репозиториях успешно выполнена. dashboard.git_gc_repos_success=Сборка мусора на всех репозиториях успешно выполнена.
dashboard.resync_all_sshkeys=Переписать файл «.ssh/authorized_keys» (осторожно: не Gogs ключи будут утеряны) dashboard.resync_all_sshkeys=Переписать файл «.ssh/authorized_keys» (осторожно: не Gogs ключи будут утеряны)
dashboard.resync_all_sshkeys_success=Были успешно переписаны все открытые ключи. dashboard.resync_all_sshkeys_success=Были успешно переписаны все открытые ключи.
dashboard.resync_all_update_hooks=Rewrite all update hook of repositories (needed when custom config path is changed) dashboard.resync_all_update_hooks=Перезаписать все апдейт-хуки этого репозитория (необходимо, когда изменен путь до папки конфигураций)
dashboard.resync_all_update_hooks_success=All repositories' update hook have been rewritten successfully. dashboard.resync_all_update_hooks_success=Апдейт-хуки всех репозиториев успешно перезаписаны.
dashboard.server_uptime=Время непрерывной работы сервера dashboard.server_uptime=Время непрерывной работы сервера
dashboard.current_goroutine=Текущий Goroutines dashboard.current_goroutine=Текущий Goroutines
dashboard.current_memory_usage=Текущее использование памяти dashboard.current_memory_usage=Текущее использование памяти
dashboard.total_memory_allocated=Всего памяти выделено dashboard.total_memory_allocated=Всего памяти выделено
dashboard.memory_obtained=Memory Obtained dashboard.memory_obtained=Памяти использовано
dashboard.pointer_lookup_times=Pointer Lookup Times dashboard.pointer_lookup_times=Запросов указателя
dashboard.memory_allocate_times=Memory Allocate Times dashboard.memory_allocate_times=Выделений памяти
dashboard.memory_free_times=Memory Free Times dashboard.memory_free_times=Освобождений памяти
dashboard.current_heap_usage=Текущее использование кучи dashboard.current_heap_usage=Текущее использование кучи
dashboard.heap_memory_obtained=Heap Memory Obtained dashboard.heap_memory_obtained=Получено динамической памяти
dashboard.heap_memory_idle=Heap Memory Idle dashboard.heap_memory_idle=Не используется динамической памяти
dashboard.heap_memory_in_use=Кучи памяти в работе dashboard.heap_memory_in_use=Кучи памяти в работе
dashboard.heap_memory_released=Heap Memory Released dashboard.heap_memory_released=Освобождено динамической памяти
dashboard.heap_objects=Heap Objects dashboard.heap_objects=Объектов динамической памяти
dashboard.bootstrap_stack_usage=Bootstrap Stack Usage dashboard.bootstrap_stack_usage=Использование стека загрузчика
dashboard.stack_memory_obtained=Stack Memory Obtained dashboard.stack_memory_obtained=Память, занятая под стек
dashboard.mspan_structures_usage=MSpan Structures Usage dashboard.mspan_structures_usage=Использование структур MSpan
dashboard.mspan_structures_obtained=MSpan Structures Obtained dashboard.mspan_structures_obtained=Получено структур MSpan
dashboard.mcache_structures_usage=MCache Structures Usage dashboard.mcache_structures_usage=Использование структур MCache
dashboard.mcache_structures_obtained=MCache Structures Obtained dashboard.mcache_structures_obtained=Получено структур MCache
dashboard.profiling_bucket_hash_table_obtained=Profiling Bucket Hash Table Obtained dashboard.profiling_bucket_hash_table_obtained=Хеш-таблиц получено при профилировании
dashboard.gc_metadata_obtained=GC Metadada Obtained dashboard.gc_metadata_obtained=Получены метаданные сборщика мусора
dashboard.other_system_allocation_obtained=Other System Allocation Obtained dashboard.other_system_allocation_obtained=Получено других системных выделений памяти
dashboard.next_gc_recycle=Next GC Recycle dashboard.next_gc_recycle=Следующая очистка сборщика мусора
dashboard.last_gc_time=Since Last GC Time dashboard.last_gc_time=Прошло с последнего сбора мусора
dashboard.total_gc_time=Total GC Pause dashboard.total_gc_time=Итоговое время GC
dashboard.total_gc_pause=Total GC Pause dashboard.total_gc_pause=Итоговая задержка GC
dashboard.last_gc_pause=Last GC Pause dashboard.last_gc_pause=Последняя пауза сборщика мусора
dashboard.gc_times=GC Times dashboard.gc_times=Количество сборок мусора
users.user_manage_panel=User Manage Panel users.user_manage_panel=Панель управления пользователями
users.new_account=Создать новый аккаунт users.new_account=Создать новый аккаунт
users.name=Имя users.name=Имя
users.activated=Активирован users.activated=Активирован
users.admin=Администратор users.admin=Администратор
users.repos=Репозитории users.repos=Репозитории
users.created=Создано users.created=Создано
users.send_register_notify=Send Registration Notification To User users.send_register_notify=Отправить пользователю уведомление о регистрации
users.new_success=New account '%s' has been created successfully. users.new_success=Новая учетная запись '%s' успешно создана.
users.edit=Редактировать users.edit=Редактировать
users.auth_source=Authentication Source users.auth_source=Источник аутентификации
users.local=Локальный users.local=Локальный
users.auth_login_name=Authentication Login Name users.auth_login_name=Логин для авторизации
users.password_helper=Leave it empty to remain unchanged. users.password_helper=Оставьте пустым, чтобы оставить без изменений.
users.update_profile_success=Профиль учетной записи обновлен успешно. users.update_profile_success=Профиль учетной записи обновлен успешно.
users.edit_account=Изменение учетной записи users.edit_account=Изменение учетной записи
users.is_activated=Эта учетная запись активирована users.is_activated=Эта учетная запись активирована
users.is_admin=У этой учетной записи есть права администратора users.is_admin=У этой учетной записи есть права администратора
users.allow_git_hook=Пользователь имеет право создать Git перехватчик users.allow_git_hook=Пользователь имеет право создать Git перехватчик
users.allow_import_local=Пользователь имеет право импортировать локальные репозитории
users.update_profile=Обновить профиль учетной записи users.update_profile=Обновить профиль учетной записи
users.delete_account=Удалить эту учетную запись users.delete_account=Удалить эту учетную запись
users.still_own_repo=На вашем аккаунте все еще остается как минимум один репозиторий, сначала вам нужно удалить или передать его. users.still_own_repo=На вашем аккаунте все еще остается как минимум один репозиторий, сначала вам нужно удалить или передать его.
users.still_has_org=This account still has membership in at least one organization, you have to leave or delete the organizations first. users.still_has_org=Эта учетная запись все еще является членом как минимум одной организации. Для продолжения, покиньте или удалите эту организацию.
users.deletion_success=Account has been deleted successfully! users.deletion_success=Учетная запись успешно удалена!
orgs.org_manage_panel=Управление группами orgs.org_manage_panel=Управление группами
orgs.name=Имя orgs.name=Имя
orgs.teams=Команды orgs.teams=Команды
orgs.members=Участники orgs.members=Участники
repos.repo_manage_panel=Repository Manage Panel repos.repo_manage_panel=Панель управления репозиторием
repos.owner=Владелец repos.owner=Владелец
repos.name=Имя repos.name=Имя
repos.private=Приватный repos.private=Приватный
@ -823,47 +829,47 @@ repos.watches=Следят
repos.stars=В избранном repos.stars=В избранном
repos.issues=Вопросы repos.issues=Вопросы
auths.auth_manage_panel=Authentication Manage Panel auths.auth_manage_panel=Панель управления аутнентификациями
auths.new=Add New Source auths.new=Добавить новый источник
auths.name=Имя auths.name=Имя
auths.type=Тип auths.type=Тип
auths.enabled=Включено auths.enabled=Включено
auths.updated=Обновлено auths.updated=Обновлено
auths.auth_type=Authentication Type auths.auth_type=Тип аутентификации
auths.auth_name=Authentication Name auths.auth_name=Имя аутентификации
auths.domain=Домен auths.domain=Домен
auths.host=Хост auths.host=Хост
auths.port=Порт auths.port=Порт
auths.bind_dn=Bind DN auths.bind_dn=Привязать DN
auths.bind_password=Bind Password auths.bind_password=Привязать пароль
auths.bind_password_helper=Warning: This password is stored in plain text. Do not use a high privileged account. auths.bind_password_helper=Внимание: Этот пароль сохранен в небезопасном виде. Не используйте высоко-привилегированную учетную запись.
auths.user_base=User Search Base auths.user_base=База для поиска пользователя
auths.user_dn=User DN auths.user_dn=DN пользователя
auths.attribute_name=First name attribute auths.attribute_name=Имя аттрибута
auths.attribute_surname=Surname attribute auths.attribute_surname=Фамилия аттрибута
auths.attribute_mail=E-mail attribute auths.attribute_mail=Электронная почта аттрибута
auths.filter=User Filter auths.filter=Фильтр пользователя
auths.admin_filter=Admin Filter auths.admin_filter=Фильтр администратора
auths.ms_ad_sa=Ms Ad SA auths.ms_ad_sa=Ms Ad SA
auths.smtp_auth=SMTP Authentication Type auths.smtp_auth=Тип аутентификации SMTP
auths.smtphost=Узел SMTP auths.smtphost=Узел SMTP
auths.smtpport=SMTP-порт auths.smtpport=SMTP-порт
auths.allowed_domains=Allowed Domains auths.allowed_domains=Разрешенные домены
auths.allowed_domains_helper=Leave it empty to not restrict any domains. Multiple domains should be separated by comma ','. auths.allowed_domains_helper=Оставьте пустым чтобы не ограничивать домены. Несколько доменов должны быть разделены запятыми ','.
auths.enable_tls=Включение шифрования TLS auths.enable_tls=Включение шифрования TLS
auths.skip_tls_verify=Skip TLS Verify auths.skip_tls_verify=Пропустить проверку TLS
auths.pam_service_name=PAM Service Name auths.pam_service_name=Имя службы PAM
auths.enable_auto_register=Включить автоматическую регистрацию auths.enable_auto_register=Включить автоматическую регистрацию
auths.tips=Советы auths.tips=Советы
auths.edit=Edit Authentication Setting auths.edit=Изменить параметры канала аутентификации
auths.activated=Эта аутентификация активирована auths.activated=Эта аутентификация активирована
auths.new_success=New authentication '%s' has been added successfully. auths.new_success=Новый канал аутентификации '%s' успешно создан.
auths.update_success=Authentication setting has been updated successfully. auths.update_success=Настройки канала аутентификации успешно сохранены.
auths.update=Update Authentication Setting auths.update=Обновить параметры аутентификации
auths.delete=Delete This Authentication auths.delete=Удалить этот канал аутентификации
auths.delete_auth_title=Authentication Deletion auths.delete_auth_title=Удаление канала аутентификации
auths.delete_auth_desc=This authentication is going to be deleted, do you want to continue? auths.delete_auth_desc=Этот канал аутентификации будет удален. Вы уверены что хотите продолжить?
auths.deletion_success=Authentication has been deleted successfully! auths.deletion_success=Канал аутентификации успешно удален!
config.server_config=Конфигурация сервера config.server_config=Конфигурация сервера
config.app_name=Имя приложения config.app_name=Имя приложения
@ -874,11 +880,11 @@ config.offline_mode=Автономный режим
config.disable_router_log=Отключение журнала маршрутизатора config.disable_router_log=Отключение журнала маршрутизатора
config.run_user=Запуск пользователем config.run_user=Запуск пользователем
config.run_mode=Режим выполнения config.run_mode=Режим выполнения
config.repo_root_path=Repository Root Path config.repo_root_path=Путь до корня репозитория
config.static_file_root_path=Static File Root Path config.static_file_root_path=Статичный путь до файла
config.log_file_root_path=Log File Root Path config.log_file_root_path=Путь до папки с логами
config.script_type=Тип сценария config.script_type=Тип сценария
config.reverse_auth_user=Reverse Authentication User config.reverse_auth_user=Заголовок с именем пользователя для авторизации на reverse proxy
config.db_config=Конфигурация базы данных config.db_config=Конфигурация базы данных
config.db_type=Тип config.db_type=Тип
config.db_host=Хост config.db_host=Хост
@ -887,20 +893,20 @@ config.db_user=Пользователь
config.db_ssl_mode=Режим SSL config.db_ssl_mode=Режим SSL
config.db_ssl_mode_helper=(только для «postgres») config.db_ssl_mode_helper=(только для «postgres»)
config.db_path=Путь config.db_path=Путь
config.db_path_helper=(for "sqlite3" and "tidb") config.db_path_helper=(для "SQLite3" и "TiDB")
config.service_config=Service Configuration config.service_config=Сервисная конфигурация
config.register_email_confirm=Require E-mail Confirmation config.register_email_confirm=Требуется подтверждение по электронной почте
config.disable_register=Отключить регистрацию config.disable_register=Отключить регистрацию
config.show_registration_button=Show Register Button config.show_registration_button=Показать кнопку регистрации
config.require_sign_in_view=Для просмотра необходима авторизация config.require_sign_in_view=Для просмотра необходима авторизация
config.enable_cache_avatar=Кешировать аватар config.enable_cache_avatar=Кешировать аватар
config.mail_notify=Почтовые уведомления config.mail_notify=Почтовые уведомления
config.disable_key_size_check=Disable Minimum Key Size Check config.disable_key_size_check=Отключить проверку на минимальный размер ключа
config.enable_captcha=Enable Captcha config.enable_captcha=Включить капчу
config.active_code_lives=Active Code Lives config.active_code_lives=Время жизни кода для активации
config.reset_password_code_lives=Reset Password Code Lives config.reset_password_code_lives=Время жизни кода сброса пароля
config.webhook_config=Настройка автоматического обновления репозиции config.webhook_config=Настройка автоматического обновления репозиции
config.queue_length=Queue Length config.queue_length=Длина очереди
config.deliver_timeout=Задержка доставки config.deliver_timeout=Задержка доставки
config.skip_tls_verify=Пропустить TLS проверка config.skip_tls_verify=Пропустить TLS проверка
config.mailer_config=Настройки почты config.mailer_config=Настройки почты
@ -912,20 +918,20 @@ config.mailer_user=Пользователь
config.oauth_config=Конфигурация OAuth config.oauth_config=Конфигурация OAuth
config.oauth_enabled=Включено config.oauth_enabled=Включено
config.cache_config=Настройки кеша config.cache_config=Настройки кеша
config.cache_adapter=Cache Adapter config.cache_adapter=Адаптер кэша
config.cache_interval=Cache Interval config.cache_interval=Интервал кэширования
config.cache_conn=Cache Connection config.cache_conn=Подключение кэша
config.session_config=Session Configuration config.session_config=Конфигурация сессии
config.session_provider=Session Provider config.session_provider=Провайдер сессии
config.provider_config=Provider Config config.provider_config=Конфигурация провайдера
config.cookie_name=Имя файла cookie config.cookie_name=Имя файла cookie
config.enable_set_cookie=Enable Set Cookie config.enable_set_cookie=Включить установку cookies
config.gc_interval_time=GC Interval Time config.gc_interval_time=Интервал работы сборщика мусора
config.session_life_time=Время жизни сессии config.session_life_time=Время жизни сессии
config.https_only=Только HTTPS config.https_only=Только HTTPS
config.cookie_life_time=Время жизни файла cookie config.cookie_life_time=Время жизни файла cookie
config.picture_config=Настройка изображения config.picture_config=Настройка изображения
config.picture_service=Picture Service config.picture_service=Сервис изображений
config.disable_gravatar=Отключить Gravatar config.disable_gravatar=Отключить Gravatar
config.log_config=Конфигурация журнала config.log_config=Конфигурация журнала
config.log_mode=Режим журналирования config.log_mode=Режим журналирования
@ -935,7 +941,7 @@ monitor.name=Имя
monitor.schedule=Расписание monitor.schedule=Расписание
monitor.next=В следующий раз monitor.next=В следующий раз
monitor.previous=Предыдущий раз monitor.previous=Предыдущий раз
monitor.execute_times=Execute Times monitor.execute_times=Количество выполнений
monitor.process=Запущенные процессы monitor.process=Запущенные процессы
monitor.desc=Описание monitor.desc=Описание
monitor.start=Момент начала monitor.start=Момент начала
@ -950,40 +956,40 @@ notices.delete_success=Системное уведомление успешно
[action] [action]
create_repo=создан репозиторий <a href="%s"> %s</a> create_repo=создан репозиторий <a href="%s"> %s</a>
rename_repo=renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a> rename_repo=репозиторий переименован из <code>%[1]s</code>на <a href="%[2]s">%[3]s</a>
commit_repo=pushed to <a href="%s/src/%s">%[2]s</a> at <a href="%[1]s">%[3]s</a> commit_repo=запушил <a href="%s/src/%s">%[2]s</a> в <a href="%[1]s">%[3]s</a>
create_issue=`opened issue <a href="%s/issues/%s">%s#%[2]s</a>` create_issue=`открытый вопрос <a href="%s/issues/%s">%s#%[2]</a>`
create_pull_request=`created pull request <a href="%s/pulls/%s">%s#%[2]s</a>` create_pull_request=`созданный пулл-реквест <a href="%s/pulls/%s">%s#%[2]s</a>`
comment_issue=`commented on issue <a href="%s/issues/%s">%s#%[2]s</a>` comment_issue=`прокомментировал(а) вопрос <a href="%s/issues/%s">%s#%[2]s</a>`
merge_pull_request=`merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>` merge_pull_request=`слил пул реквест <a href="%s/pulls/%s">%s#%[2]s</a>`
transfer_repo=transfered repository <code>%s</code> to <a href="%s">%s</a> transfer_repo=перенес репозиторий <code>%s</code> в <a href="%s">%s</a>
push_tag=pushed tag <a href="%s/src/%s">%[2]s</a> to <a href="%[1]s">%[3]s</a> push_tag=запушил тэг <a href="%s/src/%s">%[2]s</a> в <a href="%[1]s">%[3]s</a>
compare_2_commits=Просмотреть сравнение двух коммитов compare_2_commits=Просмотреть сравнение двух коммитов
[tool] [tool]
ago=назад ago=назад
from_now=from now from_now=с этого момента
now=сейчас now=сейчас
1s=1 second %s 1s=1 секунду %s
1m=1 минута %s 1m=1 минута %s
1h=1 час %s 1h=1 час %s
1d=1 день %s 1d=1 день %s
1w=1 неделя %s 1w=1 неделя %s
1mon=1 month %s 1mon=1 месяц %s
1y=1 год %s 1y=1 год %s
seconds=%d секунд %s seconds=%d секунд %s
minutes=%d минут %s minutes=%d минут %s
hours=%d часов %s hours=%d часов %s
days=%d дней %s days=%d дней %s
weeks=%d weeks %s weeks=недель %s: %d
months=%d months %s months=месяцев %s: %d
years=%d years %s years=лет %s: %d
raw_seconds=секунд raw_seconds=секунд
raw_minutes=минут raw_minutes=минут
[dropzone] [dropzone]
default_message=Drop files here or click to upload. default_message=Перетащите файл сюда, или кликните для загрузки.
invalid_input_type=You can't upload files of this type. invalid_input_type=Вы не можете загружать файлы этого типа.
file_too_big=File size({{filesize}} MB) exceeds maximum size({{maxFilesize}} MB). file_too_big=Размер файла ({{filesize}} МБ) больше чем максимальный размер ({{maxFilesize}} МБ).
remove_file=Remove file remove_file=Удалить файл

16
conf/locale/locale_zh-CN.ini

@ -148,7 +148,6 @@ forgot_password=忘记密码
forget_password=忘记密码? forget_password=忘记密码?
sign_up_now=还没帐户?马上注册。 sign_up_now=还没帐户?马上注册。
confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 <b>%s</b>,请检查您的收件箱并在 %d 小时内完成确认注册操作。 confirmation_mail_sent_prompt=一封新的确认邮件已经被发送至 <b>%s</b>,请检查您的收件箱并在 %d 小时内完成确认注册操作。
sign_in_to_account=登录到您的帐户
active_your_account=激活您的帐户 active_your_account=激活您的帐户
resent_limit_prompt=对不起,您请求发送激活邮件过于频繁,请等待 3 分钟后再试! resent_limit_prompt=对不起,您请求发送激活邮件过于频繁,请等待 3 分钟后再试!
has_unconfirmed_mail=%s 您好,系统检测到您有一封发送至 <b>%s</b> 但未被确认的邮件。如果您未收到激活邮件,或需要重新发送,请单击下方的按钮。 has_unconfirmed_mail=%s 您好,系统检测到您有一封发送至 <b>%s</b> 但未被确认的邮件。如果您未收到激活邮件,或需要重新发送,请单击下方的按钮。
@ -192,6 +191,7 @@ min_size_error=长度最小为 %s 个字符。
max_size_error=长度最大为 %s 个字符。 max_size_error=长度最大为 %s 个字符。
email_error=不是一个有效的邮箱地址。 email_error=不是一个有效的邮箱地址。
url_error=不是一个有效的 URL。 url_error=不是一个有效的 URL。
include_error=必须包含子字符串 '%s'。
unknown_error=未知错误: unknown_error=未知错误:
captcha_incorrect=验证码未匹配。 captcha_incorrect=验证码未匹配。
password_not_match=密码与确认密码未匹配。 password_not_match=密码与确认密码未匹配。
@ -334,6 +334,7 @@ repo_name=仓库名称
repo_name_helper=伟大的仓库名称一般都较短、令人深刻并且 <strong>独一无二</strong> 的。 repo_name_helper=伟大的仓库名称一般都较短、令人深刻并且 <strong>独一无二</strong> 的。
visibility=可见性 visibility=可见性
visiblity_helper=该仓库为 <span class="ui red text">私有的</span> visiblity_helper=该仓库为 <span class="ui red text">私有的</span>
visiblity_helper_forced=网站管理员已强制要求所有新建仓库必须为 <span class="ui red text">私有的</span>
visiblity_fork_helper=(修改该值将会影响到所有派生仓库) visiblity_fork_helper=(修改该值将会影响到所有派生仓库)
fork_repo=派生仓库 fork_repo=派生仓库
fork_from=派生自 fork_from=派生自
@ -359,6 +360,7 @@ migrate_type_helper=该仓库将是一个 <span class="text blue">镜像</span>
migrate_repo=迁移仓库 migrate_repo=迁移仓库
migrate.clone_address=克隆地址 migrate.clone_address=克隆地址
migrate.clone_address_desc=该地址可以是 HTTP/HTTPS/GIT URL 或本地服务器路径。 migrate.clone_address_desc=该地址可以是 HTTP/HTTPS/GIT URL 或本地服务器路径。
migrate.permission_denied=您没有获得导入本地仓库的权限。
migrate.invalid_local_path=无效的本地路径,不存在或不是一个目录! migrate.invalid_local_path=无效的本地路径,不存在或不是一个目录!
forked_from=派生自 forked_from=派生自
@ -454,9 +456,9 @@ issues.num_comments=%d 条评论
issues.commented_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 评论` issues.commented_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 评论`
issues.no_content=这个人很懒,什么都没留下。 issues.no_content=这个人很懒,什么都没留下。
issues.close_issue=关闭 issues.close_issue=关闭
issues.close_comment_issue=关闭并评论 issues.close_comment_issue=评论并关闭
issues.reopen_issue=重新开启 issues.reopen_issue=重新开启
issues.reopen_comment_issue=重新开启并评论 issues.reopen_comment_issue=评论并重新开启
issues.create_comment=评论 issues.create_comment=评论
issues.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭` issues.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭`
issues.reopened_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 重新开启` issues.reopened_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 重新开启`
@ -498,10 +500,12 @@ pulls.reopen_to_merge=请重新开启合并请求来完成合并操作。
pulls.merged=已合并 pulls.merged=已合并
pulls.has_merged=该合并请求已经成功合并! pulls.has_merged=该合并请求已经成功合并!
pulls.data_broken=该合并请求的数据由于派生仓库的相关信息被删除而被破坏。 pulls.data_broken=该合并请求的数据由于派生仓库的相关信息被删除而被破坏。
pulls.is_checking=该合并请求正在进行冲突检查,请稍后再刷新页面。
pulls.can_auto_merge_desc=您可以实现该合并请求的自动合并操作。 pulls.can_auto_merge_desc=您可以实现该合并请求的自动合并操作。
pulls.cannot_auto_merge_desc=因为代码提交存在冲突,您无法对该合并请求执行自动合并操作。 pulls.cannot_auto_merge_desc=因为代码提交存在冲突,您无法对该合并请求执行自动合并操作。
pulls.cannot_auto_merge_helper=请使用命令行工具来解决冲突。 pulls.cannot_auto_merge_helper=请使用命令行工具来解决冲突。
pulls.merge_pull_request=合并请求 pulls.merge_pull_request=合并请求
pulls.open_unmerged_pull_exists=`由于已经存在来自相同仓库和合并信息的未合并请求(#%d),您无法执行重新开启操作。`
milestones.new=新的里程碑 milestones.new=新的里程碑
milestones.open_tab=%d 开启中 milestones.open_tab=%d 开启中
@ -516,7 +520,7 @@ milestones.title=标题
milestones.desc=描述 milestones.desc=描述
milestones.due_date=截止日期(可选) milestones.due_date=截止日期(可选)
milestones.clear=清除 milestones.clear=清除
milestones.invalid_due_date_format=截止日期的格式错误,必须是 'year-mm-dd' 的形式。 milestones.invalid_due_date_format=截止日期的格式错误,必须是 'yyyy-mm-dd' 的形式。
milestones.create_success=里程碑 '%s' 创建成功! milestones.create_success=里程碑 '%s' 创建成功!
milestones.edit=编辑里程碑 milestones.edit=编辑里程碑
milestones.edit_subheader=使用更加清晰的描述来帮助人们更好地理解里程碑的作用。 milestones.edit_subheader=使用更加清晰的描述来帮助人们更好地理解里程碑的作用。
@ -648,6 +652,7 @@ release.tag_name_already_exist=已经存在使用相同标签进行发布的版
[org] [org]
org_name_holder=组织名称 org_name_holder=组织名称
org_full_name_holder=组织全名
org_name_helper=伟大的组织都有一个简短而寓意深刻的名字。 org_name_helper=伟大的组织都有一个简短而寓意深刻的名字。
create_org=创建组织 create_org=创建组织
repo_updated=最后更新于 repo_updated=最后更新于
@ -803,7 +808,8 @@ users.update_profile_success=该用户信息更新成功!
users.edit_account=编辑用户信息 users.edit_account=编辑用户信息
users.is_activated=该用户已被激活 users.is_activated=该用户已被激活
users.is_admin=该用户具有管理员权限 users.is_admin=该用户具有管理员权限
users.allow_git_hook=该帐户具有创建 Git 钩子的权限 users.allow_git_hook=该用户具有创建 Git 钩子的权限
users.allow_import_local=该用户具有导入本地仓库的权限
users.update_profile=更新用户信息 users.update_profile=更新用户信息
users.delete_account=删除该用户 users.delete_account=删除该用户
users.still_own_repo=该帐户仍然是某些仓库的拥有者,您必须先转移或删除它们才能执行删除帐户操作! users.still_own_repo=该帐户仍然是某些仓库的拥有者,您必须先转移或删除它们才能执行删除帐户操作!

16
conf/locale/locale_zh-HK.ini

@ -13,7 +13,7 @@ version=當前版本
page=頁面 page=頁面
template=模版 template=模版
language=語言選項 language=語言選項
create_new=Create... create_new=Create...Cebuano
user_profile_and_more=用戶信息及更多 user_profile_and_more=用戶信息及更多
signed_in_as=已登錄用戶 signed_in_as=已登錄用戶
@ -53,7 +53,7 @@ code=程式碼
[install] [install]
install=安裝頁面 install=安裝頁面
title=首次執行安裝程序 title=首次執行安裝程序
docker_helper=If you're running Gogs inside Docker, please read <a target="_blank" href="%s">Guidelines</a> carefully before you change anything in this page! docker_helper=English/tagalog
requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB. requite_db_desc=Gogs requires MySQL, PostgreSQL, SQLite3 or TiDB.
db_title=數據庫設置 db_title=數據庫設置
db_type=數據庫類型 db_type=數據庫類型
@ -111,7 +111,7 @@ admin_title=管理員帳號設置
admin_name=管理員用戶名 admin_name=管理員用戶名
admin_password=管理員密碼 admin_password=管理員密碼
confirm_password=確認密碼 confirm_password=確認密碼
admin_email=管理員郵箱 admin_email=Admin E-mail
install_gogs=立即安裝 install_gogs=立即安裝
test_git_failed=無法識別 'git' 命令:%v test_git_failed=無法識別 'git' 命令:%v
sqlite3_not_available=您所使用的發行版本不支持 SQLite3,請從 %s 下載官方構建版,而不是 gobuild 版本。 sqlite3_not_available=您所使用的發行版本不支持 SQLite3,請從 %s 下載官方構建版,而不是 gobuild 版本。
@ -148,7 +148,6 @@ forgot_password=忘記密碼
forget_password=忘記密碼? forget_password=忘記密碼?
sign_up_now=還沒帳戶?馬上註冊。 sign_up_now=還沒帳戶?馬上註冊。
confirmation_mail_sent_prompt=一封新的確認郵件已經被發送至 <b>%s</b>,請檢查您的收件箱並在 %d 小時內完成確認註冊操作。 confirmation_mail_sent_prompt=一封新的確認郵件已經被發送至 <b>%s</b>,請檢查您的收件箱並在 %d 小時內完成確認註冊操作。
sign_in_to_account=Sign in to your account
active_your_account=激活您的帳戶 active_your_account=激活您的帳戶
resent_limit_prompt=對不起,您請求發送激活郵件過於頻繁,請等待 3 分鐘後再試! resent_limit_prompt=對不起,您請求發送激活郵件過於頻繁,請等待 3 分鐘後再試!
has_unconfirmed_mail=%s 您好,您有一封發送至( <b>%s</b>) 但未被確認的郵件。如果您未收到激活郵件,或需要重新發送,請單擊下方的按鈕。 has_unconfirmed_mail=%s 您好,您有一封發送至( <b>%s</b>) 但未被確認的郵件。如果您未收到激活郵件,或需要重新發送,請單擊下方的按鈕。
@ -192,6 +191,7 @@ min_size_error=長度最小為 %s 個字符。
max_size_error=長度最大為 %s 個字符。 max_size_error=長度最大為 %s 個字符。
email_error=不是一個有效的郵箱地址。 email_error=不是一個有效的郵箱地址。
url_error=不是一個有效的 URL。 url_error=不是一個有效的 URL。
include_error=` must contain substring '%s'.`
unknown_error=未知錯誤: unknown_error=未知錯誤:
captcha_incorrect=驗證碼未匹配。 captcha_incorrect=驗證碼未匹配。
password_not_match=密碼與確認密碼未匹配。 password_not_match=密碼與確認密碼未匹配。
@ -334,6 +334,7 @@ repo_name=倉庫名稱
repo_name_helper=偉大的倉庫名稱一般都較短、令人深刻並且 <strong>獨一無二</strong> 的。 repo_name_helper=偉大的倉庫名稱一般都較短、令人深刻並且 <strong>獨一無二</strong> 的。
visibility=可見度 visibility=可見度
visiblity_helper=該倉庫為 <span class="ui red text">私有的</span> visiblity_helper=該倉庫為 <span class="ui red text">私有的</span>
visiblity_helper_forced=Site admin has forced all new repositories to be <span class="ui red text">Private</span>
visiblity_fork_helper=(修改該值將會影響到所有派生倉庫) visiblity_fork_helper=(修改該值將會影響到所有派生倉庫)
fork_repo=派生倉庫 fork_repo=派生倉庫
fork_from=派生自 fork_from=派生自
@ -359,6 +360,7 @@ migrate_type_helper=該倉庫將是一個 <span class="text blue">鏡像</span>
migrate_repo=遷移倉庫 migrate_repo=遷移倉庫
migrate.clone_address=複製地址 migrate.clone_address=複製地址
migrate.clone_address_desc=該地址可以是 HTTP/HTTPS/GIT URL 或本地服務器路徑。 migrate.clone_address_desc=該地址可以是 HTTP/HTTPS/GIT URL 或本地服務器路徑。
migrate.permission_denied=You are not allowed to import local repositories.
migrate.invalid_local_path=無效的本地路徑,該路徑不存在或不是一個目錄! migrate.invalid_local_path=無效的本地路徑,該路徑不存在或不是一個目錄!
forked_from=派生自 forked_from=派生自
@ -498,10 +500,12 @@ pulls.reopen_to_merge=Please reopen this pull request to perform merge operation
pulls.merged=Merged pulls.merged=Merged
pulls.has_merged=This pull request has been merged successfully! pulls.has_merged=This pull request has been merged successfully!
pulls.data_broken=Data of this pull request has been broken due to deletion of fork information. pulls.data_broken=Data of this pull request has been broken due to deletion of fork information.
pulls.is_checking=The conflict checking is still in progress, please refresh page in few moments.
pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request. pulls.can_auto_merge_desc=You can perform auto-merge operation on this pull request.
pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits. pulls.cannot_auto_merge_desc=You can't perform auto-merge operation because there are conflicts between commits.
pulls.cannot_auto_merge_helper=Please use command line tool to solve it. pulls.cannot_auto_merge_helper=Please use command line tool to solve it.
pulls.merge_pull_request=Merge Pull Request pulls.merge_pull_request=Merge Pull Request
pulls.open_unmerged_pull_exists=`You can't perform reopen operation because there is already an open pull request (#%d) from same repository with same merge information and is waiting for merging.`
milestones.new=新的里程碑 milestones.new=新的里程碑
milestones.open_tab=%d 開啟中 milestones.open_tab=%d 開啟中
@ -516,7 +520,7 @@ milestones.title=標題
milestones.desc=描述 milestones.desc=描述
milestones.due_date=截止日期(可選) milestones.due_date=截止日期(可選)
milestones.clear=清除 milestones.clear=清除
milestones.invalid_due_date_format=截止日期的格式錯誤,必須是 'year-mm-dd' 的形式。 milestones.invalid_due_date_format=截止日期的格式錯誤,必須是 'yyyy-mm-dd' 的形式。
milestones.create_success=里程碑 '%s' 創建成功! milestones.create_success=里程碑 '%s' 創建成功!
milestones.edit=編輯里程碑 milestones.edit=編輯里程碑
milestones.edit_subheader=使用更加清晰的描述來幫助人們更好地理解里程碑的作用。 milestones.edit_subheader=使用更加清晰的描述來幫助人們更好地理解里程碑的作用。
@ -648,6 +652,7 @@ release.tag_name_already_exist=已經存在使用相同標籤的發佈版本。
[org] [org]
org_name_holder=組織名稱 org_name_holder=組織名稱
org_full_name_holder=Organization Full Name
org_name_helper=偉大的組織都有一個簡短而寓意深刻的名字。 org_name_helper=偉大的組織都有一個簡短而寓意深刻的名字。
create_org=創建組織 create_org=創建組織
repo_updated=最後更新於 repo_updated=最後更新於
@ -804,6 +809,7 @@ users.edit_account=編輯用戶信息
users.is_activated=該用戶已被激活 users.is_activated=該用戶已被激活
users.is_admin=該用戶具有管理員權限 users.is_admin=該用戶具有管理員權限
users.allow_git_hook=該帳戶具有創建 Git 鉤子的權限 users.allow_git_hook=該帳戶具有創建 Git 鉤子的權限
users.allow_import_local=This account has permissions to import local repositories
users.update_profile=更新用戶信息 users.update_profile=更新用戶信息
users.delete_account=刪除該用戶 users.delete_account=刪除該用戶
users.still_own_repo=該帳戶仍然是某些倉庫的擁有者,您必須先轉移或刪除它們才能執行刪除帳戶操作! users.still_own_repo=該帳戶仍然是某些倉庫的擁有者,您必須先轉移或刪除它們才能執行刪除帳戶操作!

12
docker-compose.yml

@ -1,12 +0,0 @@
web:
build: .
links:
- mysql
ports:
- "3000:3000"
mysql:
image: mysql
environment:
- MYSQL_ROOT_PASSWORD=gogs
- MYSQL_DATABASE=gogs

2
docker/build.sh

@ -1,4 +1,6 @@
#!/bin/sh #!/bin/sh
set -x
set -e
# Set temp environment vars # Set temp environment vars
export GOPATH=/tmp/go export GOPATH=/tmp/go

1
docker/s6/gogs/setup

@ -20,3 +20,4 @@ ln -sf /data/gogs/data ./data
ln -sf /data/git /home/git ln -sf /data/git /home/git
chown -R git:git /data /app/gogs ~git/ chown -R git:git /data /app/gogs ~git/
chmod 0755 /data /data/gogs ~git/

3
docker/s6/openssh/setup

@ -23,4 +23,5 @@ fi
# Set correct right to ssh keys # Set correct right to ssh keys
chown -R root:root /data/ssh/* chown -R root:root /data/ssh/*
chmod 600 /data/ssh/* chmod 0700 /data/ssh
chmod 0600 /data/ssh/*

7
docker/s6/syslogd/run

@ -0,0 +1,7 @@
#!/bin/sh
if test -f ./setup; then
source ./setup
fi
exec gosu root /sbin/syslogd -nS -O-

64
docker/start.sh

@ -1,28 +1,56 @@
#!/bin/sh #!/bin/sh
# Cleanup SOCAT services and s6 event folder create_socat_links() {
# On start and on shutdown in case container has been killed # Bind linked docker container to localhost socket using socat
rm -rf $(find /app/gogs/docker/s6/ -name 'event') USED_PORT="3000:22"
rm -rf /app/gogs/docker/s6/SOCAT_* while read NAME ADDR PORT; do
if test -z "$NAME$ADDR$PORT"; then
continue
elif echo $USED_PORT | grep -E "(^|:)$PORT($|:)" > /dev/null; then
echo "init:socat | Can't bind linked container ${NAME} to localhost, port ${PORT} already in use" 1>&2
else
SERV_FOLDER=/app/gogs/docker/s6/SOCAT_${NAME}_${PORT}
mkdir -p ${SERV_FOLDER}
CMD="socat -ls TCP4-LISTEN:${PORT},fork,reuseaddr TCP4:${ADDR}:${PORT}"
echo -e "#!/bin/sh\nexec $CMD" > ${SERV_FOLDER}/run
chmod +x ${SERV_FOLDER}/run
USED_PORT="${USED_PORT}:${PORT}"
echo "init:socat | Linked container ${NAME} will be binded to localhost on port ${PORT}" 1>&2
fi
done << EOT
$(env | sed -En 's|(.*)_PORT_([0-9]+)_TCP=tcp://(.*):([0-9]+)|\1 \3 \4|p')
EOT
}
# Create VOLUME subfolder cleanup() {
for f in /data/gogs/data /data/gogs/conf /data/gogs/log /data/git /data/ssh; do # Cleanup SOCAT services and s6 event folder
if ! test -d $f; then # On start and on shutdown in case container has been killed
mkdir -p $f rm -rf $(find /app/gogs/docker/s6/ -name 'event')
fi rm -rf /app/gogs/docker/s6/SOCAT_*
done }
# Bind linked docker container to localhost socket using socat create_volume_subfolder() {
env | sed -En 's|(.*)_PORT_([0-9]*)_TCP=tcp://(.*):(.*)|\1_\2 socat -ls TCP4-LISTEN:\2,fork,reuseaddr TCP4:\3:\4|p' | \ # Create VOLUME subfolder
while read NAME CMD; do for f in /data/gogs/data /data/gogs/conf /data/gogs/log /data/git /data/ssh; do
mkdir -p /app/gogs/docker/s6/SOCAT_$NAME if ! test -d $f; then
echo -e "#!/bin/sh\nexec $CMD" > /app/gogs/docker/s6/SOCAT_$NAME/run mkdir -p $f
chmod +x /app/gogs/docker/s6/SOCAT_$NAME/run fi
done done
}
cleanup
create_volume_subfolder
LINK=$(echo "$SOCAT_LINK" | tr '[:upper:]' '[:lower:]')
if [ "$LINK" = "false" -o "$LINK" = "0" ]; then
echo "init:socat | Will not try to create socat links as requested" 1>&2
else
create_socat_links
fi
# Exec CMD or S6 by default if nothing present # Exec CMD or S6 by default if nothing present
if [ $# -gt 0 ];then if [ $# -gt 0 ];then
exec "$@" exec "$@"
else else
exec /usr/bin/s6-svscan /app/gogs/docker/s6/ exec /bin/s6-svscan /app/gogs/docker/s6/
fi fi

4
gogs.go

@ -4,7 +4,7 @@
// 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.
// Gogs(Go Git Service) is a painless self-hosted Git Service written in Go. // Gogs (Go Git Service) is a painless self-hosted Git Service.
package main package main
import ( import (
@ -17,7 +17,7 @@ import (
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
) )
const APP_VER = "0.6.16.1018 Beta" const APP_VER = "0.7.16.1117 Beta"
func init() { func init() {
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())

28
models/access.go

@ -67,9 +67,8 @@ func HasAccess(u *User, repo *Repository, testMode AccessMode) (bool, error) {
return hasAccess(x, u, repo, testMode) return hasAccess(x, u, repo, testMode)
} }
// GetAccessibleRepositories finds all repositories where a user has access to, // GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own.
// besides he/she owns. func (u *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) {
func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) {
accesses := make([]*Access, 0, 10) accesses := make([]*Access, 0, 10)
if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil { if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil {
return nil, err return nil, err
@ -80,7 +79,7 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) {
repo, err := GetRepositoryByID(access.RepoID) repo, err := GetRepositoryByID(access.RepoID)
if err != nil { if err != nil {
if IsErrRepoNotExist(err) { if IsErrRepoNotExist(err) {
log.Error(4, "%v", err) log.Error(4, "GetRepositoryByID: %v", err)
continue continue
} }
return nil, err return nil, err
@ -92,11 +91,28 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) {
} }
repos[repo] = access.Mode repos[repo] = access.Mode
} }
// FIXME: should we generate an ordered list here? Random looks weird.
return repos, nil return repos, nil
} }
// GetAccessibleRepositories finds all repositories where a user has access but does not own.
func (u *User) GetAccessibleRepositories() ([]*Repository, error) {
accesses := make([]*Access, 0, 10)
if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil {
return nil, err
}
if len(accesses) == 0 {
return []*Repository{}, nil
}
repoIDs := make([]int64, 0, len(accesses))
for _, access := range accesses {
repoIDs = append(repoIDs, access.RepoID)
}
repos := make([]*Repository, 0, len(repoIDs))
return repos, x.Where("owner_id != ?", u.Id).In("id", repoIDs).Desc("updated").Find(&repos)
}
func maxAccessMode(modes ...AccessMode) AccessMode { func maxAccessMode(modes ...AccessMode) AccessMode {
max := ACCESS_MODE_NONE max := ACCESS_MODE_NONE
for _, mode := range modes { for _, mode := range modes {

85
models/action.go

@ -14,6 +14,7 @@ import (
"time" "time"
"unicode" "unicode"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
api "github.com/kiliit/go-gogs-client" api "github.com/kiliit/go-gogs-client"
@ -136,6 +137,26 @@ func (a Action) GetIssueInfos() []string {
return strings.SplitN(a.Content, "|", 2) return strings.SplitN(a.Content, "|", 2)
} }
func (a Action) GetIssueTitle() string {
index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
issue, err := GetIssueByIndex(a.RepoID, index)
if err != nil {
log.Error(4, "GetIssueByIndex: %v", err)
return "500 when get issue"
}
return issue.Name
}
func (a Action) GetIssueContent() string {
index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
issue, err := GetIssueByIndex(a.RepoID, index)
if err != nil {
log.Error(4, "GetIssueByIndex: %v", err)
return "500 when get issue"
}
return issue.Content
}
func newRepoAction(e Engine, u *User, repo *Repository) (err error) { func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
if err = notifyWatchers(e, &Action{ if err = notifyWatchers(e, &Action{
ActUserID: u.Id, ActUserID: u.Id,
@ -147,7 +168,7 @@ func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
RepoName: repo.Name, RepoName: repo.Name,
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
}); err != nil { }); err != nil {
return fmt.Errorf("notify watchers '%d/%s': %v", u.Id, repo.ID, err) return fmt.Errorf("notify watchers '%d/%d': %v", u.Id, repo.ID, err)
} }
log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name) log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name)
@ -187,8 +208,48 @@ func issueIndexTrimRight(c rune) bool {
return !unicode.IsDigit(c) return !unicode.IsDigit(c)
} }
type PushCommit struct {
Sha1 string
Message string
AuthorEmail string
AuthorName string
}
type PushCommits struct {
Len int
Commits []*PushCommit
CompareUrl string
avatars map[string]string
}
func NewPushCommits() *PushCommits {
return &PushCommits{
avatars: make(map[string]string),
}
}
// AvatarLink tries to match user in database with e-mail
// in order to show custom avatar, and falls back to general avatar link.
func (push *PushCommits) AvatarLink(email string) string {
_, ok := push.avatars[email]
if !ok {
u, err := GetUserByEmail(email)
if err != nil {
push.avatars[email] = base.AvatarLink(email)
if !IsErrUserNotExist(err) {
log.Error(4, "GetUserByEmail: %v", err)
}
} else {
push.avatars[email] = u.AvatarLink()
}
}
return push.avatars[email]
}
// updateIssuesCommit checks if issues are manipulated by commit message. // updateIssuesCommit checks if issues are manipulated by commit message.
func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*base.PushCommit) error { func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*PushCommit) error {
// Commits are appended in the reverse order. // Commits are appended in the reverse order.
for i := len(commits) - 1; i >= 0; i-- { for i := len(commits) - 1; i >= 0; i-- {
c := commits[i] c := commits[i]
@ -322,7 +383,7 @@ func CommitRepoAction(
repoID int64, repoID int64,
repoUserName, repoName string, repoUserName, repoName string,
refFullName string, refFullName string,
commit *base.PushCommits, commit *PushCommits,
oldCommitID string, newCommitID string) error { oldCommitID string, newCommitID string) error {
u, err := GetUserByID(userID) u, err := GetUserByID(userID)
@ -337,12 +398,18 @@ func CommitRepoAction(
return fmt.Errorf("GetOwner: %v", err) return fmt.Errorf("GetOwner: %v", err)
} }
// Change repository bare status and update last updated time.
repo.IsBare = false
if err = UpdateRepository(repo, false); err != nil {
return fmt.Errorf("UpdateRepository: %v", err)
}
isNewBranch := false isNewBranch := false
opType := COMMIT_REPO opType := COMMIT_REPO
// Check it's tag push or branch. // Check it's tag push or branch.
if strings.HasPrefix(refFullName, "refs/tags/") { if strings.HasPrefix(refFullName, "refs/tags/") {
opType = PUSH_TAG opType = PUSH_TAG
commit = &base.PushCommits{} commit = &PushCommits{}
} else { } else {
// if not the first commit, set the compareUrl // if not the first commit, set the compareUrl
if !strings.HasPrefix(oldCommitID, "0000000") { if !strings.HasPrefix(oldCommitID, "0000000") {
@ -351,12 +418,10 @@ func CommitRepoAction(
isNewBranch = true isNewBranch = true
} }
// Change repository bare status and update last updated time. // NOTE: limit to detect latest 100 commits.
repo.IsBare = false if len(commit.Commits) > 100 {
if err = UpdateRepository(repo, false); err != nil { commit.Commits = commit.Commits[len(commit.Commits)-100:]
return fmt.Errorf("UpdateRepository: %v", err)
} }
if err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits); err != nil { if err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits); err != nil {
log.Error(4, "updateIssuesCommit: %v", err) log.Error(4, "updateIssuesCommit: %v", err)
} }
@ -488,7 +553,7 @@ func transferRepoAction(e Engine, actUser, oldOwner, newOwner *User, repo *Repos
IsPrivate: repo.IsPrivate, IsPrivate: repo.IsPrivate,
Content: path.Join(oldOwner.LowerName, repo.LowerName), Content: path.Join(oldOwner.LowerName, repo.LowerName),
}); err != nil { }); err != nil {
return fmt.Errorf("notify watchers '%d/%s': %v", actUser.Id, repo.ID, err) return fmt.Errorf("notify watchers '%d/%d': %v", actUser.Id, repo.ID, err)
} }
// Remove watch for organization. // Remove watch for organization.

89
models/error.go

@ -18,7 +18,7 @@ func IsErrNameReserved(err error) bool {
} }
func (err ErrNameReserved) Error() string { func (err ErrNameReserved) Error() string {
return fmt.Sprintf("name is reserved: [name: %s]", err.Name) return fmt.Sprintf("name is reserved [name: %s]", err.Name)
} }
type ErrNamePatternNotAllowed struct { type ErrNamePatternNotAllowed struct {
@ -31,7 +31,7 @@ func IsErrNamePatternNotAllowed(err error) bool {
} }
func (err ErrNamePatternNotAllowed) Error() string { func (err ErrNamePatternNotAllowed) Error() string {
return fmt.Sprintf("name pattern is not allowed: [pattern: %s]", err.Pattern) return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
} }
// ____ ___ // ____ ___
@ -51,7 +51,7 @@ func IsErrUserAlreadyExist(err error) bool {
} }
func (err ErrUserAlreadyExist) Error() string { func (err ErrUserAlreadyExist) Error() string {
return fmt.Sprintf("user already exists: [name: %s]", err.Name) return fmt.Sprintf("user already exists [name: %s]", err.Name)
} }
type ErrUserNotExist struct { type ErrUserNotExist struct {
@ -65,7 +65,7 @@ func IsErrUserNotExist(err error) bool {
} }
func (err ErrUserNotExist) Error() string { func (err ErrUserNotExist) Error() string {
return fmt.Sprintf("user does not exist: [uid: %d, name: %s]", err.UID, err.Name) return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name)
} }
type ErrEmailAlreadyUsed struct { type ErrEmailAlreadyUsed struct {
@ -78,7 +78,7 @@ func IsErrEmailAlreadyUsed(err error) bool {
} }
func (err ErrEmailAlreadyUsed) Error() string { func (err ErrEmailAlreadyUsed) Error() string {
return fmt.Sprintf("e-mail has been used: [email: %s]", err.Email) return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
} }
type ErrUserOwnRepos struct { type ErrUserOwnRepos struct {
@ -91,7 +91,7 @@ func IsErrUserOwnRepos(err error) bool {
} }
func (err ErrUserOwnRepos) Error() string { func (err ErrUserOwnRepos) Error() string {
return fmt.Sprintf("user still has ownership of repositories: [uid: %d]", err.UID) return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
} }
type ErrUserHasOrgs struct { type ErrUserHasOrgs struct {
@ -104,7 +104,7 @@ func IsErrUserHasOrgs(err error) bool {
} }
func (err ErrUserHasOrgs) Error() string { func (err ErrUserHasOrgs) Error() string {
return fmt.Sprintf("user still has membership of organizations: [uid: %d]", err.UID) return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
} }
// __________ ___. .__ .__ ____ __. // __________ ___. .__ .__ ____ __.
@ -124,7 +124,7 @@ func IsErrKeyNotExist(err error) bool {
} }
func (err ErrKeyNotExist) Error() string { func (err ErrKeyNotExist) Error() string {
return fmt.Sprintf("public key does not exist: [id: %d]", err.ID) return fmt.Sprintf("public key does not exist [id: %d]", err.ID)
} }
type ErrKeyAlreadyExist struct { type ErrKeyAlreadyExist struct {
@ -138,7 +138,7 @@ func IsErrKeyAlreadyExist(err error) bool {
} }
func (err ErrKeyAlreadyExist) Error() string { func (err ErrKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists: [owner_id: %d, content: %s]", err.OwnerID, err.Content) return fmt.Sprintf("public key already exists [owner_id: %d, content: %s]", err.OwnerID, err.Content)
} }
type ErrKeyNameAlreadyUsed struct { type ErrKeyNameAlreadyUsed struct {
@ -152,7 +152,7 @@ func IsErrKeyNameAlreadyUsed(err error) bool {
} }
func (err ErrKeyNameAlreadyUsed) Error() string { func (err ErrKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists: [owner_id: %d, name: %s]", err.OwnerID, err.Name) return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
} }
type ErrDeployKeyAlreadyExist struct { type ErrDeployKeyAlreadyExist struct {
@ -166,7 +166,7 @@ func IsErrDeployKeyAlreadyExist(err error) bool {
} }
func (err ErrDeployKeyAlreadyExist) Error() string { func (err ErrDeployKeyAlreadyExist) Error() string {
return fmt.Sprintf("public key already exists: [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID) return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
} }
type ErrDeployKeyNameAlreadyUsed struct { type ErrDeployKeyNameAlreadyUsed struct {
@ -180,7 +180,7 @@ func IsErrDeployKeyNameAlreadyUsed(err error) bool {
} }
func (err ErrDeployKeyNameAlreadyUsed) Error() string { func (err ErrDeployKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists: [repo_id: %d, name: %s]", err.RepoID, err.Name) return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
} }
// _____ ___________ __ // _____ ___________ __
@ -200,7 +200,7 @@ func IsErrAccessTokenNotExist(err error) bool {
} }
func (err ErrAccessTokenNotExist) Error() string { func (err ErrAccessTokenNotExist) Error() string {
return fmt.Sprintf("access token does not exist: [sha: %s]", err.SHA) return fmt.Sprintf("access token does not exist [sha: %s]", err.SHA)
} }
// ________ .__ __ .__ // ________ .__ __ .__
@ -220,7 +220,7 @@ func IsErrLastOrgOwner(err error) bool {
} }
func (err ErrLastOrgOwner) Error() string { func (err ErrLastOrgOwner) Error() string {
return fmt.Sprintf("user is the last member of owner team: [uid: %d]", err.UID) return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
} }
// __________ .__ __ // __________ .__ __
@ -259,6 +259,61 @@ func (err ErrRepoAlreadyExist) Error() string {
return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name) return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
} }
type ErrInvalidCloneAddr struct {
IsURLError bool
IsInvalidPath bool
IsPermissionDenied bool
}
func IsErrInvalidCloneAddr(err error) bool {
_, ok := err.(ErrInvalidCloneAddr)
return ok
}
func (err ErrInvalidCloneAddr) Error() string {
return fmt.Sprintf("invalid clone address [is_url_error: %v, is_invalid_path: %v, is_permission_denied: %v]",
err.IsURLError, err.IsInvalidPath, err.IsPermissionDenied)
}
type ErrUpdateTaskNotExist struct {
UUID string
}
func IsErrUpdateTaskNotExist(err error) bool {
_, ok := err.(ErrUpdateTaskNotExist)
return ok
}
func (err ErrUpdateTaskNotExist) Error() string {
return fmt.Sprintf("update task does not exist [uuid: %s]", err.UUID)
}
type ErrReleaseAlreadyExist struct {
TagName string
}
func IsErrReleaseAlreadyExist(err error) bool {
_, ok := err.(ErrReleaseAlreadyExist)
return ok
}
func (err ErrReleaseAlreadyExist) Error() string {
return fmt.Sprintf("Release tag already exist [tag_name: %s]", err.TagName)
}
type ErrReleaseNotExist struct {
TagName string
}
func IsErrReleaseNotExist(err error) bool {
_, ok := err.(ErrReleaseNotExist)
return ok
}
func (err ErrReleaseNotExist) Error() string {
return fmt.Sprintf("Release tag does not exist [tag_name: %s]", err.TagName)
}
// __ __ ___. .__ __ // __ __ ___. .__ __
// / \ / \ ____\_ |__ | |__ ____ ____ | | __ // / \ / \ ____\_ |__ | |__ ____ ____ | | __
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ / // \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
@ -310,7 +365,7 @@ func (err ErrIssueNotExist) Error() string {
type ErrPullRequestNotExist struct { type ErrPullRequestNotExist struct {
ID int64 ID int64
PullID int64 IssueID int64
HeadRepoID int64 HeadRepoID int64
BaseRepoID int64 BaseRepoID int64
HeadBarcnh string HeadBarcnh string
@ -323,8 +378,8 @@ func IsErrPullRequestNotExist(err error) bool {
} }
func (err ErrPullRequestNotExist) Error() string { func (err ErrPullRequestNotExist) Error() string {
return fmt.Sprintf("pull request does not exist [id: %d, pull_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]", return fmt.Sprintf("pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
err.ID, err.PullID, err.HeadRepoID, err.BaseRepoID, err.HeadBarcnh, err.BaseBranch) err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBarcnh, err.BaseBranch)
} }
// _________ __ // _________ __

40
models/git_diff.go

@ -37,6 +37,7 @@ const (
DIFF_FILE_ADD = iota + 1 DIFF_FILE_ADD = iota + 1
DIFF_FILE_CHANGE DIFF_FILE_CHANGE
DIFF_FILE_DEL DIFF_FILE_DEL
DIFF_FILE_RENAME
) )
type DiffLine struct { type DiffLine struct {
@ -57,12 +58,14 @@ type DiffSection struct {
type DiffFile struct { type DiffFile struct {
Name string Name string
OldName string
Index int Index int
Addition, Deletion int Addition, Deletion int
Type int Type int
IsCreated bool IsCreated bool
IsDeleted bool IsDeleted bool
IsBin bool IsBin bool
IsRenamed bool
Sections []*DiffSection Sections []*DiffSection
} }
@ -94,7 +97,7 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff
var i int var i int
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
// fmt.Println(i, line)
if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") { if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") {
continue continue
} }
@ -158,17 +161,27 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff
// Get new file. // Get new file.
if strings.HasPrefix(line, DIFF_HEAD) { if strings.HasPrefix(line, DIFF_HEAD) {
beg := len(DIFF_HEAD) middle := -1
a := line[beg : (len(line)-beg)/2+beg]
// Note: In case file name is surrounded by double quotes(it happens only in git-shell).
hasQuote := strings.Index(line, `\"`) > -1
if hasQuote {
line = strings.Replace(line, `\"`, `"`, -1)
middle = strings.Index(line, ` "b/`)
} else {
middle = strings.Index(line, " b/")
}
// In case file name is surrounded by double quotes(it happens only in git-shell). beg := len(DIFF_HEAD)
if a[0] == '"' { a := line[beg+2 : middle]
b := line[middle+3:]
if hasQuote {
a = a[1 : len(a)-1] a = a[1 : len(a)-1]
a = strings.Replace(a, `\"`, `"`, -1) b = b[1 : len(b)-1]
} }
curFile = &DiffFile{ curFile = &DiffFile{
Name: a[strings.Index(a, "/")+1:], Name: a,
Index: len(diff.Files) + 1, Index: len(diff.Files) + 1,
Type: DIFF_FILE_CHANGE, Type: DIFF_FILE_CHANGE,
Sections: make([]*DiffSection, 0, 10), Sections: make([]*DiffSection, 0, 10),
@ -180,16 +193,17 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff
switch { switch {
case strings.HasPrefix(scanner.Text(), "new file"): case strings.HasPrefix(scanner.Text(), "new file"):
curFile.Type = DIFF_FILE_ADD curFile.Type = DIFF_FILE_ADD
curFile.IsDeleted = false
curFile.IsCreated = true curFile.IsCreated = true
case strings.HasPrefix(scanner.Text(), "deleted"): case strings.HasPrefix(scanner.Text(), "deleted"):
curFile.Type = DIFF_FILE_DEL curFile.Type = DIFF_FILE_DEL
curFile.IsCreated = false
curFile.IsDeleted = true curFile.IsDeleted = true
case strings.HasPrefix(scanner.Text(), "index"): case strings.HasPrefix(scanner.Text(), "index"):
curFile.Type = DIFF_FILE_CHANGE curFile.Type = DIFF_FILE_CHANGE
curFile.IsCreated = false case strings.HasPrefix(scanner.Text(), "similarity index 100%"):
curFile.IsDeleted = false curFile.Type = DIFF_FILE_RENAME
curFile.IsRenamed = true
curFile.OldName = curFile.Name
curFile.Name = b
} }
if curFile.Type > 0 { if curFile.Type > 0 {
break break
@ -244,10 +258,10 @@ func GetDiffRange(repoPath, beforeCommitId string, afterCommitId string, maxline
cmd = exec.Command("git", "show", afterCommitId) cmd = exec.Command("git", "show", afterCommitId)
} else { } else {
c, _ := commit.Parent(0) c, _ := commit.Parent(0)
cmd = exec.Command("git", "diff", c.Id.String(), afterCommitId) cmd = exec.Command("git", "diff", "-M", c.ID.String(), afterCommitId)
} }
} else { } else {
cmd = exec.Command("git", "diff", beforeCommitId, afterCommitId) cmd = exec.Command("git", "diff", "-M", beforeCommitId, afterCommitId)
} }
cmd.Dir = repoPath cmd.Dir = repoPath
cmd.Stdout = wr cmd.Stdout = wr

28
models/issue.go

@ -92,15 +92,6 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
if err != nil { if err != nil {
log.Error(3, "GetUserByID[%d]: %v", i.ID, err) log.Error(3, "GetUserByID[%d]: %v", i.ID, err)
} }
case "is_pull":
if !i.IsPull {
return
}
i.PullRequest, err = GetPullRequestByIssueID(i.ID)
if err != nil {
log.Error(3, "GetPullRequestByIssueID[%d]: %v", i.ID, err)
}
case "created": case "created":
i.Created = regulateTimeZone(i.Created) i.Created = regulateTimeZone(i.Created)
} }
@ -233,7 +224,7 @@ func (i *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (err er
} }
i.IsClosed = isClosed i.IsClosed = isClosed
if err = updateIssue(e, i); err != nil { if err = updateIssueCols(e, i, "is_closed"); err != nil {
return err return err
} else if err = updateIssueUsersByStatus(e, i.ID, isClosed); err != nil { } else if err = updateIssueUsersByStatus(e, i.ID, isClosed); err != nil {
return err return err
@ -282,6 +273,15 @@ func (i *Issue) ChangeStatus(doer *User, isClosed bool) (err error) {
return sess.Commit() return sess.Commit()
} }
func (i *Issue) GetPullRequest() (err error) {
if i.PullRequest != nil {
return nil
}
i.PullRequest, err = GetPullRequestByIssueID(i.ID)
return err
}
// It's caller's responsibility to create action. // It's caller's responsibility to create action.
func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string, isPull bool) (err error) { func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string, isPull bool) (err error) {
if _, err = e.Insert(issue); err != nil { if _, err = e.Insert(issue); err != nil {
@ -818,11 +818,17 @@ func updateIssue(e Engine, issue *Issue) error {
return err return err
} }
// UpdateIssue updates information of issue. // UpdateIssue updates all fields of given issue.
func UpdateIssue(issue *Issue) error { func UpdateIssue(issue *Issue) error {
return updateIssue(x, issue) return updateIssue(x, issue)
} }
// updateIssueCols updates specific fields of given issue.
func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
_, err := e.Id(issue.ID).Cols(cols...).Update(issue)
return err
}
func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error { func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error {
_, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID) _, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID)
return err return err

53
models/migrations/migrations.go

@ -11,6 +11,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"path/filepath"
"strings" "strings"
"time" "time"
@ -66,6 +67,7 @@ var migrations = []Migration{
NewMigration("generate issue-label from issue", issueToIssueLabel), // V6 -> V7:v0.6.4 NewMigration("generate issue-label from issue", issueToIssueLabel), // V6 -> V7:v0.6.4
NewMigration("refactor attachment table", attachmentRefactor), // V7 -> V8:v0.6.4 NewMigration("refactor attachment table", attachmentRefactor), // V7 -> V8:v0.6.4
NewMigration("rename pull request fields", renamePullRequestFields), // V8 -> V9:v0.6.16 NewMigration("rename pull request fields", renamePullRequestFields), // V8 -> V9:v0.6.16
NewMigration("clean up migrate repo info", cleanUpMigrateRepoInfo), // V9 -> V10:v0.6.20
} }
// Migrate database to current version // Migrate database to current version
@ -454,7 +456,7 @@ func trimCommitActionAppUrlPrefix(x *xorm.Engine) error {
pushCommits = new(PushCommits) pushCommits = new(PushCommits)
if err = json.Unmarshal(action["content"], pushCommits); err != nil { if err = json.Unmarshal(action["content"], pushCommits); err != nil {
return fmt.Errorf("unmarshal action content[%s]: %v", actID, err) return fmt.Errorf("unmarshal action content[%d]: %v", actID, err)
} }
infos := strings.Split(pushCommits.CompareUrl, "/") infos := strings.Split(pushCommits.CompareUrl, "/")
@ -465,7 +467,7 @@ func trimCommitActionAppUrlPrefix(x *xorm.Engine) error {
p, err := json.Marshal(pushCommits) p, err := json.Marshal(pushCommits)
if err != nil { if err != nil {
return fmt.Errorf("marshal action content[%s]: %v", actID, err) return fmt.Errorf("marshal action content[%d]: %v", actID, err)
} }
if _, err = sess.Id(actID).Update(&Action{ if _, err = sess.Id(actID).Update(&Action{
@ -653,3 +655,50 @@ func renamePullRequestFields(x *xorm.Engine) (err error) {
return sess.Commit() return sess.Commit()
} }
func cleanUpMigrateRepoInfo(x *xorm.Engine) (err error) {
type (
User struct {
ID int64 `xorm:"pk autoincr"`
LowerName string
}
Repository struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64
LowerName string
}
)
repos := make([]*Repository, 0, 25)
if err = x.Where("is_mirror=?", false).Find(&repos); err != nil {
return fmt.Errorf("select all non-mirror repositories: %v", err)
}
var user *User
for _, repo := range repos {
user = &User{ID: repo.OwnerID}
has, err := x.Get(user)
if err != nil {
return fmt.Errorf("get owner of repository[%d - %d]: %v", repo.ID, repo.OwnerID, err)
} else if !has {
continue
}
configPath := filepath.Join(setting.RepoRootPath, user.LowerName, repo.LowerName+".git/config")
// In case repository file is somehow missing.
if !com.IsFile(configPath) {
continue
}
cfg, err := ini.Load(configPath)
if err != nil {
return fmt.Errorf("open config file: %v", err)
}
cfg.DeleteSection("remote \"origin\"")
if err = cfg.SaveToIndent(configPath, "\t"); err != nil {
return fmt.Errorf("save config file: %v", err)
}
}
return nil
}

2
models/models.go

@ -90,7 +90,7 @@ func init() {
new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
new(Notice), new(EmailAddress)) new(Notice), new(EmailAddress))
gonicNames := []string{"UID", "SSL"} gonicNames := []string{"SSL"}
for _, name := range gonicNames { for _, name := range gonicNames {
core.LintGonicMapper[name] = true core.LintGonicMapper[name] = true
} }

65
models/publickey.go

@ -13,7 +13,6 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
@ -38,20 +37,7 @@ var (
) )
var sshOpLocker = sync.Mutex{} var sshOpLocker = sync.Mutex{}
var SSHPath string // SSH directory.
var (
SSHPath string // SSH directory.
appPath string // Execution(binary) path.
)
// exePath returns the executable path.
func exePath() (string, error) {
file, err := exec.LookPath(os.Args[0])
if err != nil {
return "", err
}
return filepath.Abs(file)
}
// homeDir returns the home directory of current user. // homeDir returns the home directory of current user.
func homeDir() string { func homeDir() string {
@ -63,16 +49,9 @@ func homeDir() string {
} }
func init() { func init() {
var err error
if appPath, err = exePath(); err != nil {
log.Fatal(4, "fail to get app path: %v\n", err)
}
appPath = strings.Replace(appPath, "\\", "/", -1)
// Determine and create .ssh path. // Determine and create .ssh path.
SSHPath = filepath.Join(homeDir(), ".ssh") SSHPath = filepath.Join(homeDir(), ".ssh")
if err = os.MkdirAll(SSHPath, 0700); err != nil { if err := os.MkdirAll(SSHPath, 0700); err != nil {
log.Fatal(4, "fail to create '%s': %v", SSHPath, err) log.Fatal(4, "fail to create '%s': %v", SSHPath, err)
} }
} }
@ -114,17 +93,7 @@ func (k *PublicKey) OmitEmail() string {
// GetAuthorizedString generates and returns formatted public key string for authorized_keys file. // GetAuthorizedString generates and returns formatted public key string for authorized_keys file.
func (key *PublicKey) GetAuthorizedString() string { func (key *PublicKey) GetAuthorizedString() string {
return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.ID, setting.CustomConf, key.Content) return fmt.Sprintf(_TPL_PUBLICK_KEY, setting.AppPath, key.ID, setting.CustomConf, key.Content)
}
var minimumKeySizes = map[string]int{
"(ED25519)": 256,
"(ECDSA)": 256,
"(NTRU)": 1087,
"(MCE)": 1702,
"(McE)": 1702,
"(RSA)": 1024,
"(DSA)": 1024,
} }
func extractTypeFromBase64Key(key string) (string, error) { func extractTypeFromBase64Key(key string) (string, error) {
@ -228,9 +197,9 @@ func CheckPublicKeyString(content string) (_ string, err error) {
tmpFile.Close() tmpFile.Close()
// Check if ssh-keygen recognizes its contents. // Check if ssh-keygen recognizes its contents.
stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath) stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-lf", tmpPath)
if err != nil { if err != nil {
return "", errors.New("ssh-keygen -l -f: " + stderr) return "", errors.New("ssh-keygen -lf: " + stderr)
} else if len(stdout) < 2 { } else if len(stdout) < 2 {
return "", errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout) return "", errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout)
} }
@ -251,9 +220,10 @@ func CheckPublicKeyString(content string) (_ string, err error) {
if keySize == 0 { if keySize == 0 {
return "", errors.New("cannot get key size of the given key") return "", errors.New("cannot get key size of the given key")
} }
keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1])
if minimumKeySize := minimumKeySizes[keyType]; minimumKeySize == 0 { keyType := strings.Trim(sshKeygenOutput[len(sshKeygenOutput)-1], " ()\n")
return "", errors.New("sorry, unrecognized public key type") if minimumKeySize := setting.Service.MinimumKeySizes[keyType]; minimumKeySize == 0 {
return "", fmt.Errorf("unrecognized public key type: %s", keyType)
} else if keySize < minimumKeySize { } else if keySize < minimumKeySize {
return "", fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize) return "", fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize)
} }
@ -321,9 +291,9 @@ func addKey(e Engine, key *PublicKey) (err error) {
if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil { if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil {
return err return err
} }
stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath) stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
if err != nil { if err != nil {
return errors.New("ssh-keygen -l -f: " + stderr) return errors.New("ssh-keygen -lf: " + stderr)
} else if len(stdout) < 2 { } else if len(stdout) < 2 {
return errors.New("not enough output for calculating fingerprint: " + stdout) return errors.New("not enough output for calculating fingerprint: " + stdout)
} }
@ -382,6 +352,19 @@ func GetPublicKeyByID(keyID int64) (*PublicKey, error) {
return key, nil return key, nil
} }
// SearchPublicKeyByContent searches content as prefix (leak e-mail part)
// and returns public key found.
func SearchPublicKeyByContent(content string) (*PublicKey, error) {
key := new(PublicKey)
has, err := x.Where("content like ?", content+"%").Get(key)
if err != nil {
return nil, err
} else if !has {
return nil, ErrKeyNotExist{}
}
return key, nil
}
// ListPublicKeys returns a list of public keys belongs to given user. // ListPublicKeys returns a list of public keys belongs to given user.
func ListPublicKeys(uid int64) ([]*PublicKey, error) { func ListPublicKeys(uid int64) ([]*PublicKey, error) {
keys := make([]*PublicKey, 0, 5) keys := make([]*PublicKey, 0, 5)

434
models/pull.go

@ -6,7 +6,6 @@ package models
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path" "path"
"strings" "strings"
@ -18,6 +17,7 @@ import (
"github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/git"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/process"
"github.com/gogits/gogs/modules/setting"
) )
type PullRequestType int type PullRequestType int
@ -45,45 +45,25 @@ type PullRequest struct {
Issue *Issue `xorm:"-"` Issue *Issue `xorm:"-"`
Index int64 Index int64
HeadRepoID int64 HeadRepoID int64
HeadRepo *Repository `xorm:"-"` HeadRepo *Repository `xorm:"-"`
BaseRepoID int64 BaseRepoID int64
HeadUserName string BaseRepo *Repository `xorm:"-"`
HeadBranch string HeadUserName string
BaseBranch string HeadBranch string
MergeBase string `xorm:"VARCHAR(40)"` BaseBranch string
MergedCommitID string `xorm:"VARCHAR(40)"` MergeBase string `xorm:"VARCHAR(40)"`
HasMerged bool HasMerged bool
Merged time.Time MergedCommitID string `xorm:"VARCHAR(40)"`
MergerID int64 Merged time.Time
Merger *User `xorm:"-"` MergerID int64
Merger *User `xorm:"-"`
} }
// Note: don't try to get Pull because will end up recursive querying. // Note: don't try to get Pull because will end up recursive querying.
func (pr *PullRequest) AfterSet(colName string, _ xorm.Cell) { func (pr *PullRequest) AfterSet(colName string, _ xorm.Cell) {
var err error
switch colName { switch colName {
case "head_repo_id":
// FIXME: shouldn't show error if it's known that head repository has been removed.
pr.HeadRepo, err = GetRepositoryByID(pr.HeadRepoID)
if err != nil {
log.Error(3, "GetRepositoryByID[%d]: %v", pr.ID, err)
}
case "merger_id":
if !pr.HasMerged {
return
}
pr.Merger, err = GetUserByID(pr.MergerID)
if err != nil {
if IsErrUserNotExist(err) {
pr.MergerID = -1
pr.Merger = NewFakeUser()
} else {
log.Error(3, "GetUserByID[%d]: %v", pr.ID, err)
}
}
case "merged": case "merged":
if !pr.HasMerged { if !pr.HasMerged {
return return
@ -93,6 +73,50 @@ func (pr *PullRequest) AfterSet(colName string, _ xorm.Cell) {
} }
} }
func (pr *PullRequest) getHeadRepo(e Engine) (err error) {
pr.HeadRepo, err = getRepositoryByID(e, pr.HeadRepoID)
if err != nil && !IsErrRepoNotExist(err) {
return fmt.Errorf("getRepositoryByID(head): %v", err)
}
return nil
}
func (pr *PullRequest) GetHeadRepo() (err error) {
return pr.getHeadRepo(x)
}
func (pr *PullRequest) GetBaseRepo() (err error) {
if pr.BaseRepo != nil {
return nil
}
pr.BaseRepo, err = GetRepositoryByID(pr.BaseRepoID)
if err != nil {
return fmt.Errorf("GetRepositoryByID(base): %v", err)
}
return nil
}
func (pr *PullRequest) GetMerger() (err error) {
if !pr.HasMerged || pr.Merger != nil {
return nil
}
pr.Merger, err = GetUserByID(pr.MergerID)
if IsErrUserNotExist(err) {
pr.MergerID = -1
pr.Merger = NewFakeUser()
} else if err != nil {
return fmt.Errorf("GetUserByID: %v", err)
}
return nil
}
// IsChecking returns true if this pull request is still checking conflict.
func (pr *PullRequest) IsChecking() bool {
return pr.Status == PULL_REQUEST_STATUS_CHECKING
}
// CanAutoMerge returns true if this pull request can be merged automatically. // CanAutoMerge returns true if this pull request can be merged automatically.
func (pr *PullRequest) CanAutoMerge() bool { func (pr *PullRequest) CanAutoMerge() bool {
return pr.Status == PULL_REQUEST_STATUS_MERGEABLE return pr.Status == PULL_REQUEST_STATUS_MERGEABLE
@ -107,7 +131,11 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
} }
if err = pr.Issue.changeStatus(sess, doer, true); err != nil { if err = pr.Issue.changeStatus(sess, doer, true); err != nil {
return fmt.Errorf("Pull.changeStatus: %v", err) return fmt.Errorf("Issue.changeStatus: %v", err)
}
if err = pr.getHeadRepo(sess); err != nil {
return fmt.Errorf("getHeadRepo: %v", err)
} }
headRepoPath := RepoPath(pr.HeadUserName, pr.HeadRepo.Name) headRepoPath := RepoPath(pr.HeadUserName, pr.HeadRepo.Name)
@ -132,7 +160,7 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
} }
// Clone base repo. // Clone base repo.
tmpBasePath := path.Join("data/tmp/repos", com.ToStr(time.Now().Nanosecond())+".git") tmpBasePath := path.Join(setting.AppDataPath, "tmp/repos", com.ToStr(time.Now().Nanosecond())+".git")
os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm) os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm)
defer os.RemoveAll(path.Dir(tmpBasePath)) defer os.RemoveAll(path.Dir(tmpBasePath))
@ -150,11 +178,26 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
return fmt.Errorf("git checkout: %s", stderr) return fmt.Errorf("git checkout: %s", stderr)
} }
// Pull commits. // Add head repo remote.
if _, stderr, err = process.ExecDir(-1, tmpBasePath, if _, stderr, err = process.ExecDir(-1, tmpBasePath,
fmt.Sprintf("PullRequest.Merge(git pull): %s", tmpBasePath), fmt.Sprintf("PullRequest.Merge(git remote add): %s", tmpBasePath),
"git", "pull", headRepoPath, pr.HeadBranch); err != nil { "git", "remote", "add", "head_repo", headRepoPath); err != nil {
return fmt.Errorf("git pull[%s / %s -> %s]: %s", headRepoPath, pr.HeadBranch, tmpBasePath, stderr) return fmt.Errorf("git remote add[%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
}
// Merge commits.
if _, stderr, err = process.ExecDir(-1, tmpBasePath,
fmt.Sprintf("PullRequest.Merge(git fetch): %s", tmpBasePath),
"git", "fetch", "head_repo"); err != nil {
return fmt.Errorf("git fetch[%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
}
if _, stderr, err = process.ExecDir(-1, tmpBasePath,
fmt.Sprintf("PullRequest.Merge(git merge): %s", tmpBasePath),
"git", "merge", "--no-ff", "-m",
fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch),
"head_repo/"+pr.HeadBranch); err != nil {
return fmt.Errorf("git merge[%s]: %s", tmpBasePath, stderr)
} }
// Push back to upstream. // Push back to upstream.
@ -167,6 +210,67 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
return sess.Commit() return sess.Commit()
} }
// patchConflicts is a list of conflit description from Git.
var patchConflicts = []string{
"patch does not apply",
"already exists in working directory",
"unrecognized input",
}
// testPatch checks if patch can be merged to base repository without conflit.
// FIXME: make a mechanism to clean up stable local copies.
func (pr *PullRequest) testPatch() (err error) {
if pr.BaseRepo == nil {
pr.BaseRepo, err = GetRepositoryByID(pr.BaseRepoID)
if err != nil {
return fmt.Errorf("GetRepositoryByID: %v", err)
}
}
patchPath, err := pr.BaseRepo.PatchPath(pr.Index)
if err != nil {
return fmt.Errorf("BaseRepo.PatchPath: %v", err)
}
// Fast fail if patch does not exist, this assumes data is cruppted.
if !com.IsFile(patchPath) {
log.Trace("PullRequest[%d].testPatch: ignored cruppted data", pr.ID)
return nil
}
log.Trace("PullRequest[%d].testPatch(patchPath): %s", pr.ID, patchPath)
if err := pr.BaseRepo.UpdateLocalCopy(); err != nil {
return fmt.Errorf("UpdateLocalCopy: %v", err)
}
// Checkout base branch.
_, stderr, err := process.ExecDir(-1, pr.BaseRepo.LocalCopyPath(),
fmt.Sprintf("PullRequest.Merge(git checkout): %s", pr.BaseRepo.ID),
"git", "checkout", pr.BaseBranch)
if err != nil {
return fmt.Errorf("git checkout: %s", stderr)
}
pr.Status = PULL_REQUEST_STATUS_CHECKING
_, stderr, err = process.ExecDir(-1, pr.BaseRepo.LocalCopyPath(),
fmt.Sprintf("testPatch(git apply --check): %d", pr.BaseRepo.ID),
"git", "apply", "--check", patchPath)
if err != nil {
for i := range patchConflicts {
if strings.Contains(stderr, patchConflicts[i]) {
log.Trace("PullRequest[%d].testPatch(apply): has conflit", pr.ID)
fmt.Println(stderr)
pr.Status = PULL_REQUEST_STATUS_CONFLICT
return nil
}
}
return fmt.Errorf("git apply --check: %v - %s", err, stderr)
}
return nil
}
// NewPullRequest creates new pull request with labels for repository. // NewPullRequest creates new pull request with labels for repository.
func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) { func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) {
sess := x.NewSession() sess := x.NewSession()
@ -195,36 +299,20 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
return err return err
} }
// Test apply patch. pr.Index = pull.Index
if err = repo.UpdateLocalCopy(); err != nil { if err = repo.SavePatch(pr.Index, patch); err != nil {
return fmt.Errorf("UpdateLocalCopy: %v", err) return fmt.Errorf("SavePatch: %v", err)
}
repoPath, err := repo.RepoPath()
if err != nil {
return fmt.Errorf("RepoPath: %v", err)
} }
patchPath := path.Join(repoPath, "pulls", com.ToStr(pull.ID)+".patch")
os.MkdirAll(path.Dir(patchPath), os.ModePerm) pr.BaseRepo = repo
if err = ioutil.WriteFile(patchPath, patch, 0644); err != nil { if err = pr.testPatch(); err != nil {
return fmt.Errorf("save patch: %v", err) return fmt.Errorf("testPatch: %v", err)
} }
if pr.Status == PULL_REQUEST_STATUS_CHECKING {
pr.Status = PULL_REQUEST_STATUS_MERGEABLE pr.Status = PULL_REQUEST_STATUS_MERGEABLE
_, stderr, err := process.ExecDir(-1, repo.LocalCopyPath(),
fmt.Sprintf("NewPullRequest(git apply --check): %d", repo.ID),
"git", "apply", "--check", patchPath)
if err != nil {
if strings.Contains(stderr, "patch does not apply") {
pr.Status = PULL_REQUEST_STATUS_CONFLICT
} else {
return fmt.Errorf("git apply --check: %v - %s", err, stderr)
}
} }
pr.IssueID = pull.ID pr.IssueID = pull.ID
pr.Index = pull.Index
if _, err = sess.Insert(pr); err != nil { if _, err = sess.Insert(pr); err != nil {
return fmt.Errorf("insert pull repo: %v", err) return fmt.Errorf("insert pull repo: %v", err)
} }
@ -236,7 +324,6 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
// by given head/base and repo/branch. // by given head/base and repo/branch.
func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) { func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) {
pr := new(PullRequest) pr := new(PullRequest)
has, err := x.Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?", has, err := x.Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?",
headRepoID, headBranch, baseRepoID, baseBranch, false, false). headRepoID, headBranch, baseRepoID, baseBranch, false, false).
Join("INNER", "issue", "issue.id=pull_request.issue_id").Get(pr) Join("INNER", "issue", "issue.id=pull_request.issue_id").Get(pr)
@ -249,14 +336,223 @@ func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch
return pr, nil return pr, nil
} }
// GetPullRequestByIssueID returns pull request by given issue ID. // GetUnmergedPullRequestsByHeadInfo returnss all pull requests that are open and has not been merged
func GetPullRequestByIssueID(pullID int64) (*PullRequest, error) { // by given head information (repo and branch).
func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) {
prs := make([]*PullRequest, 0, 2)
return prs, x.Where("head_repo_id=? AND head_branch=? AND has_merged=? AND issue.is_closed=?",
repoID, branch, false, false).
Join("INNER", "issue", "issue.id=pull_request.issue_id").Find(&prs)
}
// GetUnmergedPullRequestsByBaseInfo returnss all pull requests that are open and has not been merged
// by given base information (repo and branch).
func GetUnmergedPullRequestsByBaseInfo(repoID int64, branch string) ([]*PullRequest, error) {
prs := make([]*PullRequest, 0, 2)
return prs, x.Where("base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?",
repoID, branch, false, false).
Join("INNER", "issue", "issue.id=pull_request.issue_id").Find(&prs)
}
// GetPullRequestByID returns a pull request by given ID.
func GetPullRequestByID(id int64) (*PullRequest, error) {
pr := new(PullRequest) pr := new(PullRequest)
has, err := x.Where("pull_id=?", pullID).Get(pr) has, err := x.Id(id).Get(pr)
if err != nil {
return nil, err
} else if !has {
return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""}
}
return pr, nil
}
// GetPullRequestByIssueID returns pull request by given issue ID.
func GetPullRequestByIssueID(issueID int64) (*PullRequest, error) {
pr := &PullRequest{
IssueID: issueID,
}
has, err := x.Get(pr)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
return nil, ErrPullRequestNotExist{0, pullID, 0, 0, "", ""} return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
} }
return pr, nil return pr, nil
} }
// Update updates all fields of pull request.
func (pr *PullRequest) Update() error {
_, err := x.Id(pr.ID).AllCols().Update(pr)
return err
}
// Update updates specific fields of pull request.
func (pr *PullRequest) UpdateCols(cols ...string) error {
_, err := x.Id(pr.ID).Cols(cols...).Update(pr)
return err
}
var PullRequestQueue = NewUniqueQueue(setting.Repository.PullRequestQueueLength)
// UpdatePatch generates and saves a new patch.
func (pr *PullRequest) UpdatePatch() (err error) {
if err = pr.GetHeadRepo(); err != nil {
return fmt.Errorf("GetHeadRepo: %v", err)
} else if pr.HeadRepo == nil {
log.Trace("PullRequest[%d].UpdatePatch: ignored cruppted data", pr.ID)
return nil
}
if err = pr.GetBaseRepo(); err != nil {
return fmt.Errorf("GetBaseRepo: %v", err)
} else if err = pr.BaseRepo.GetOwner(); err != nil {
return fmt.Errorf("GetOwner: %v", err)
}
headRepoPath, err := pr.HeadRepo.RepoPath()
if err != nil {
return fmt.Errorf("HeadRepo.RepoPath: %v", err)
}
headGitRepo, err := git.OpenRepository(headRepoPath)
if err != nil {
return fmt.Errorf("OpenRepository: %v", err)
}
// Add a temporary remote.
tmpRemote := com.ToStr(time.Now().UnixNano())
if err = headGitRepo.AddRemote(tmpRemote, RepoPath(pr.BaseRepo.Owner.Name, pr.BaseRepo.Name)); err != nil {
return fmt.Errorf("AddRemote: %v", err)
}
defer func() {
headGitRepo.RemoveRemote(tmpRemote)
}()
remoteBranch := "remotes/" + tmpRemote + "/" + pr.BaseBranch
pr.MergeBase, err = headGitRepo.GetMergeBase(remoteBranch, pr.HeadBranch)
if err != nil {
return fmt.Errorf("GetMergeBase: %v", err)
} else if err = pr.Update(); err != nil {
return fmt.Errorf("Update: %v", err)
}
patch, err := headGitRepo.GetPatch(pr.MergeBase, pr.HeadBranch)
if err != nil {
return fmt.Errorf("GetPatch: %v", err)
}
if err = pr.BaseRepo.SavePatch(pr.Index, patch); err != nil {
return fmt.Errorf("BaseRepo.SavePatch: %v", err)
}
return nil
}
// AddToTaskQueue adds itself to pull request test task queue.
func (pr *PullRequest) AddToTaskQueue() {
go PullRequestQueue.AddFunc(pr.ID, func() {
pr.Status = PULL_REQUEST_STATUS_CHECKING
if err := pr.UpdateCols("status"); err != nil {
log.Error(5, "AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err)
}
})
}
func addHeadRepoTasks(prs []*PullRequest) {
for _, pr := range prs {
log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID)
if err := pr.UpdatePatch(); err != nil {
log.Error(4, "UpdatePatch: %v", err)
continue
}
pr.AddToTaskQueue()
}
}
// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch,
// and generate new patch for testing as needed.
func AddTestPullRequestTask(repoID int64, branch string) {
log.Trace("AddTestPullRequestTask[head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch)
prs, err := GetUnmergedPullRequestsByHeadInfo(repoID, branch)
if err != nil {
log.Error(4, "Find pull requests[head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
return
}
addHeadRepoTasks(prs)
log.Trace("AddTestPullRequestTask[base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch)
prs, err = GetUnmergedPullRequestsByBaseInfo(repoID, branch)
if err != nil {
log.Error(4, "Find pull requests[base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
return
}
for _, pr := range prs {
pr.AddToTaskQueue()
}
}
// checkAndUpdateStatus checks if pull request is possible to levaing checking status,
// and set to be either conflict or mergeable.
func (pr *PullRequest) checkAndUpdateStatus() {
// Status is not changed to conflict means mergeable.
if pr.Status == PULL_REQUEST_STATUS_CHECKING {
pr.Status = PULL_REQUEST_STATUS_MERGEABLE
}
// Make sure there is no waiting test to process before levaing the checking status.
if !PullRequestQueue.Exist(pr.ID) {
if err := pr.UpdateCols("status"); err != nil {
log.Error(4, "Update[%d]: %v", pr.ID, err)
}
}
}
// TestPullRequests checks and tests untested patches of pull requests.
// TODO: test more pull requests at same time.
func TestPullRequests() {
prs := make([]*PullRequest, 0, 10)
x.Iterate(PullRequest{
Status: PULL_REQUEST_STATUS_CHECKING,
},
func(idx int, bean interface{}) error {
pr := bean.(*PullRequest)
if err := pr.GetBaseRepo(); err != nil {
log.Error(3, "GetBaseRepo: %v", err)
return nil
}
if err := pr.testPatch(); err != nil {
log.Error(3, "testPatch: %v", err)
return nil
}
prs = append(prs, pr)
return nil
})
// Update pull request status.
for _, pr := range prs {
pr.checkAndUpdateStatus()
}
// Start listening on new test requests.
for prID := range PullRequestQueue.Queue() {
log.Trace("TestPullRequests[%v]: processing test task", prID)
PullRequestQueue.Remove(prID)
pr, err := GetPullRequestByID(com.StrTo(prID).MustInt64())
if err != nil {
log.Error(4, "GetPullRequestByID[%d]: %v", prID, err)
continue
} else if err = pr.testPatch(); err != nil {
log.Error(4, "testPatch[%d]: %v", pr.ID, err)
continue
}
pr.checkAndUpdateStatus()
}
}
func InitTestPullRequests() {
go TestPullRequests()
}

36
models/release.go

@ -5,7 +5,6 @@
package models package models
import ( import (
"errors"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -15,16 +14,11 @@ import (
"github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/git"
) )
var (
ErrReleaseAlreadyExist = errors.New("Release already exist")
ErrReleaseNotExist = errors.New("Release does not exist")
)
// Release represents a release of repository. // Release represents a release of repository.
type Release struct { type Release struct {
Id int64 ID int64 `xorm:"pk autoincr"`
RepoId int64 RepoID int64
PublisherId int64 PublisherID int64
Publisher *User `xorm:"-"` Publisher *User `xorm:"-"`
TagName string TagName string
LowerTagName string LowerTagName string
@ -47,12 +41,12 @@ func (r *Release) AfterSet(colName string, _ xorm.Cell) {
} }
// IsReleaseExist returns true if release with given tag name already exists. // IsReleaseExist returns true if release with given tag name already exists.
func IsReleaseExist(repoId int64, tagName string) (bool, error) { func IsReleaseExist(repoID int64, tagName string) (bool, error) {
if len(tagName) == 0 { if len(tagName) == 0 {
return false, nil return false, nil
} }
return x.Get(&Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)}) return x.Get(&Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)})
} }
func createTag(gitRepo *git.Repository, rel *Release) error { func createTag(gitRepo *git.Repository, rel *Release) error {
@ -64,7 +58,7 @@ func createTag(gitRepo *git.Repository, rel *Release) error {
return err return err
} }
if err = gitRepo.CreateTag(rel.TagName, commit.Id.String()); err != nil { if err = gitRepo.CreateTag(rel.TagName, commit.ID.String()); err != nil {
return err return err
} }
} else { } else {
@ -84,11 +78,11 @@ func createTag(gitRepo *git.Repository, rel *Release) error {
// CreateRelease creates a new release of repository. // CreateRelease creates a new release of repository.
func CreateRelease(gitRepo *git.Repository, rel *Release) error { func CreateRelease(gitRepo *git.Repository, rel *Release) error {
isExist, err := IsReleaseExist(rel.RepoId, rel.TagName) isExist, err := IsReleaseExist(rel.RepoID, rel.TagName)
if err != nil { if err != nil {
return err return err
} else if isExist { } else if isExist {
return ErrReleaseAlreadyExist return ErrReleaseAlreadyExist{rel.TagName}
} }
if err = createTag(gitRepo, rel); err != nil { if err = createTag(gitRepo, rel); err != nil {
@ -100,22 +94,22 @@ func CreateRelease(gitRepo *git.Repository, rel *Release) error {
} }
// GetRelease returns release by given ID. // GetRelease returns release by given ID.
func GetRelease(repoId int64, tagName string) (*Release, error) { func GetRelease(repoID int64, tagName string) (*Release, error) {
isExist, err := IsReleaseExist(repoId, tagName) isExist, err := IsReleaseExist(repoID, tagName)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !isExist { } else if !isExist {
return nil, ErrReleaseNotExist return nil, ErrReleaseNotExist{tagName}
} }
rel := &Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)} rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}
_, err = x.Get(rel) _, err = x.Get(rel)
return rel, err return rel, err
} }
// GetReleasesByRepoId returns a list of releases of repository. // GetReleasesByRepoId returns a list of releases of repository.
func GetReleasesByRepoId(repoId int64) (rels []*Release, err error) { func GetReleasesByRepoId(repoID int64) (rels []*Release, err error) {
err = x.Desc("created").Find(&rels, Release{RepoId: repoId}) err = x.Desc("created").Find(&rels, Release{RepoID: repoID})
return rels, err return rels, err
} }
@ -150,6 +144,6 @@ func UpdateRelease(gitRepo *git.Repository, rel *Release) (err error) {
if err = createTag(gitRepo, rel); err != nil { if err = createTag(gitRepo, rel); err != nil {
return err return err
} }
_, err = x.Id(rel.Id).AllCols().Update(rel) _, err = x.Id(rel.ID).AllCols().Update(rel)
return err return err
} }

160
models/repo.go

@ -23,6 +23,7 @@ import (
"github.com/Unknwon/cae/zip" "github.com/Unknwon/cae/zip"
"github.com/Unknwon/com" "github.com/Unknwon/com"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
"gopkg.in/ini.v1"
"github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/bindata" "github.com/gogits/gogs/modules/bindata"
@ -48,7 +49,7 @@ var (
Gitignores, Licenses, Readmes []string Gitignores, Licenses, Readmes []string
// Maximum items per page in forks, watchers and stars of a repo // Maximum items per page in forks, watchers and stars of a repo
ItemsPerPage = 54 ItemsPerPage = 40
) )
func LoadRepoConfig() { func LoadRepoConfig() {
@ -183,9 +184,11 @@ func (repo *Repository) AfterSet(colName string, _ xorm.Cell) {
} }
func (repo *Repository) getOwner(e Engine) (err error) { func (repo *Repository) getOwner(e Engine) (err error) {
if repo.Owner == nil { if repo.Owner != nil {
repo.Owner, err = getUserByID(e, repo.OwnerID) return nil
} }
repo.Owner, err = getUserByID(e, repo.OwnerID)
return err return err
} }
@ -298,7 +301,7 @@ func (repo *Repository) DescriptionHtml() template.HTML {
} }
func (repo *Repository) LocalCopyPath() string { func (repo *Repository) LocalCopyPath() string {
return path.Join(setting.RepoRootPath, "local", com.ToStr(repo.ID)) return path.Join(setting.AppDataPath, "tmp/local", com.ToStr(repo.ID))
} }
// UpdateLocalCopy makes sure the local copy of repository is up-to-date. // UpdateLocalCopy makes sure the local copy of repository is up-to-date.
@ -317,7 +320,7 @@ func (repo *Repository) UpdateLocalCopy() error {
} }
} else { } else {
_, stderr, err := process.ExecDir(-1, localPath, _, stderr, err := process.ExecDir(-1, localPath,
fmt.Sprintf("UpdateLocalCopy(git pull): %s", repoPath), "git", "pull") fmt.Sprintf("UpdateLocalCopy(git pull --all): %s", repoPath), "git", "pull", "--all")
if err != nil { if err != nil {
return fmt.Errorf("git pull: %v - %s", err, stderr) return fmt.Errorf("git pull: %v - %s", err, stderr)
} }
@ -326,6 +329,30 @@ func (repo *Repository) UpdateLocalCopy() error {
return nil return nil
} }
// PatchPath returns corresponding patch file path of repository by given issue ID.
func (repo *Repository) PatchPath(index int64) (string, error) {
if err := repo.GetOwner(); err != nil {
return "", err
}
return filepath.Join(RepoPath(repo.Owner.Name, repo.Name), "pulls", com.ToStr(index)+".patch"), nil
}
// SavePatch saves patch data to corresponding location by given issue ID.
func (repo *Repository) SavePatch(index int64, patch []byte) error {
patchPath, err := repo.PatchPath(index)
if err != nil {
return fmt.Errorf("PatchPath: %v", err)
}
os.MkdirAll(path.Dir(patchPath), os.ModePerm)
if err = ioutil.WriteFile(patchPath, patch, 0644); err != nil {
return fmt.Errorf("WriteFile: %v", err)
}
return nil
}
func isRepositoryExist(e Engine, u *User, repoName string) (bool, error) { func isRepositoryExist(e Engine, u *User, repoName string) (bool, error) {
has, err := e.Get(&Repository{ has, err := e.Get(&Repository{
OwnerID: u.Id, OwnerID: u.Id,
@ -353,11 +380,11 @@ func (repo *Repository) CloneLink() (cl CloneLink, err error) {
} }
if setting.SSHPort != 22 { if setting.SSHPort != 22 {
cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.SSHDomain, setting.SSHPort, repo.Owner.LowerName, repo.LowerName) cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.SSHDomain, setting.SSHPort, repo.Owner.Name, repo.Name)
} else { } else {
cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.SSHDomain, repo.Owner.LowerName, repo.LowerName) cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.SSHDomain, repo.Owner.Name, repo.Name)
} }
cl.HTTPS = fmt.Sprintf("%s%s/%s.git", setting.AppUrl, repo.Owner.LowerName, repo.LowerName) cl.HTTPS = fmt.Sprintf("%s%s/%s.git", setting.AppUrl, repo.Owner.Name, repo.Name)
return cl, nil return cl, nil
} }
@ -454,13 +481,21 @@ func MirrorRepository(repoId int64, userName, repoName, repoPath, url string) er
return nil return nil
} }
type MigrateRepoOptions struct {
Name string
Description string
IsPrivate bool
IsMirror bool
RemoteAddr string
}
// MigrateRepository migrates a existing repository from other project hosting. // MigrateRepository migrates a existing repository from other project hosting.
func MigrateRepository(u *User, name, desc string, private, mirror bool, url string) (*Repository, error) { func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
repo, err := CreateRepository(u, CreateRepoOptions{ repo, err := CreateRepository(u, CreateRepoOptions{
Name: name, Name: opts.Name,
Description: desc, Description: opts.Description,
IsPrivate: private, IsPrivate: opts.IsPrivate,
IsMirror: mirror, IsMirror: opts.IsMirror,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -470,7 +505,7 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str
tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond())) tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()))
os.MkdirAll(tmpDir, os.ModePerm) os.MkdirAll(tmpDir, os.ModePerm)
repoPath := RepoPath(u.Name, name) repoPath := RepoPath(u.Name, opts.Name)
if u.IsOrganization() { if u.IsOrganization() {
t, err := u.GetOwnerTeam() t, err := u.GetOwnerTeam()
@ -483,8 +518,8 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str
} }
repo.IsBare = false repo.IsBare = false
if mirror { if opts.IsMirror {
if err = MirrorRepository(repo.ID, u.Name, repo.Name, repoPath, url); err != nil { if err = MirrorRepository(repo.ID, u.Name, repo.Name, repoPath, opts.RemoteAddr); err != nil {
return repo, err return repo, err
} }
repo.IsMirror = true repo.IsMirror = true
@ -496,13 +531,24 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str
// FIXME: this command could for both migrate and mirror // FIXME: this command could for both migrate and mirror
_, stderr, err := process.ExecTimeout(10*time.Minute, _, stderr, err := process.ExecTimeout(10*time.Minute,
fmt.Sprintf("MigrateRepository: %s", repoPath), fmt.Sprintf("MigrateRepository: %s", repoPath),
"git", "clone", "--mirror", "--bare", "--quiet", url, repoPath) "git", "clone", "--mirror", "--bare", "--quiet", opts.RemoteAddr, repoPath)
if err != nil { if err != nil {
return repo, fmt.Errorf("git clone --mirror --bare --quiet: %v", stderr) return repo, fmt.Errorf("git clone --mirror --bare --quiet: %v", stderr)
} else if err = createUpdateHook(repoPath); err != nil { } else if err = createUpdateHook(repoPath); err != nil {
return repo, fmt.Errorf("create update hook: %v", err) return repo, fmt.Errorf("create update hook: %v", err)
} }
// Clean up mirror info which prevents "push --all".
configPath := filepath.Join(repoPath, "/config")
cfg, err := ini.Load(configPath)
if err != nil {
return repo, fmt.Errorf("open config file: %v", err)
}
cfg.DeleteSection("remote \"origin\"")
if err = cfg.SaveToIndent(configPath, "\t"); err != nil {
return repo, fmt.Errorf("save config file: %v", err)
}
// Check if repository is empty. // Check if repository is empty.
_, stderr, err = com.ExecCmdDir(repoPath, "git", "log", "-1") _, stderr, err = com.ExecCmdDir(repoPath, "git", "log", "-1")
if err != nil { if err != nil {
@ -553,7 +599,7 @@ func createUpdateHook(repoPath string) error {
hookPath := path.Join(repoPath, "hooks/update") hookPath := path.Join(repoPath, "hooks/update")
os.MkdirAll(path.Dir(hookPath), os.ModePerm) os.MkdirAll(path.Dir(hookPath), os.ModePerm)
return ioutil.WriteFile(hookPath, return ioutil.WriteFile(hookPath,
[]byte(fmt.Sprintf(_TPL_UPDATE_HOOK, setting.ScriptType, "\""+appPath+"\"", setting.CustomConf)), 0777) []byte(fmt.Sprintf(_TPL_UPDATE_HOOK, setting.ScriptType, "\""+setting.AppPath+"\"", setting.CustomConf)), 0777)
} }
type CreateRepoOptions struct { type CreateRepoOptions struct {
@ -864,9 +910,9 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error {
} }
// Remove redundant collaborators. // Remove redundant collaborators.
collaborators, err := repo.GetCollaborators() collaborators, err := repo.getCollaborators(sess)
if err != nil { if err != nil {
return fmt.Errorf("GetCollaborators: %v", err) return fmt.Errorf("getCollaborators: %v", err)
} }
// Dummy object. // Dummy object.
@ -902,9 +948,9 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error {
} }
if newOwner.IsOrganization() { if newOwner.IsOrganization() {
t, err := newOwner.GetOwnerTeam() t, err := newOwner.getOwnerTeam(sess)
if err != nil { if err != nil {
return fmt.Errorf("GetOwnerTeam: %v", err) return fmt.Errorf("getOwnerTeam: %v", err)
} else if err = t.addRepository(sess, repo); err != nil { } else if err = t.addRepository(sess, repo); err != nil {
return fmt.Errorf("add to owner team: %v", err) return fmt.Errorf("add to owner team: %v", err)
} }
@ -1070,7 +1116,7 @@ func DeleteRepository(uid, repoID int64) error {
return err return err
} else if _, err = sess.Delete(&Milestone{RepoID: repoID}); err != nil { } else if _, err = sess.Delete(&Milestone{RepoID: repoID}); err != nil {
return err return err
} else if _, err = sess.Delete(&Release{RepoId: repoID}); err != nil { } else if _, err = sess.Delete(&Release{RepoID: repoID}); err != nil {
return err return err
} else if _, err = sess.Delete(&Collaboration{RepoID: repoID}); err != nil { } else if _, err = sess.Delete(&Collaboration{RepoID: repoID}); err != nil {
return err return err
@ -1125,7 +1171,7 @@ func DeleteRepository(uid, repoID int64) error {
desc := fmt.Sprintf("delete repository files[%s]: %v", repoPath, err) desc := fmt.Sprintf("delete repository files[%s]: %v", repoPath, err)
log.Warn(desc) log.Warn(desc)
if err = CreateRepositoryNotice(desc); err != nil { if err = CreateRepositoryNotice(desc); err != nil {
log.Error(4, "add notice: %v", err) log.Error(4, "CreateRepositoryNotice: %v", err)
} }
} }
@ -1268,10 +1314,14 @@ func DeleteRepositoryArchives() error {
return x.Where("id > 0").Iterate(new(Repository), return x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error { func(idx int, bean interface{}) error {
repo := bean.(*Repository) repo := bean.(*Repository)
if err := repo.GetOwner(); err != nil { repoPath, err := repo.RepoPath()
return err if err != nil {
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepositoryArchives[%d]: %v", repo.ID, err)); err2 != nil {
log.Error(4, "CreateRepositoryNotice: %v", err2)
}
return nil
} }
return os.RemoveAll(filepath.Join(RepoPath(repo.Owner.Name, repo.Name), "archives")) return os.RemoveAll(filepath.Join(repoPath, "archives"))
}) })
} }
@ -1280,10 +1330,14 @@ func RewriteRepositoryUpdateHook() error {
return x.Where("id > 0").Iterate(new(Repository), return x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error { func(idx int, bean interface{}) error {
repo := bean.(*Repository) repo := bean.(*Repository)
if err := repo.GetOwner(); err != nil { repoPath, err := repo.RepoPath()
return err if err != nil {
if err2 := CreateRepositoryNotice(fmt.Sprintf("RewriteRepositoryUpdateHook[%d]: %v", repo.ID, err)); err2 != nil {
log.Error(4, "CreateRepositoryNotice: %v", err2)
}
return nil
} }
return createUpdateHook(RepoPath(repo.Owner.Name, repo.Name)) return createUpdateHook(repoPath)
}) })
} }
@ -1450,6 +1504,12 @@ func CheckRepoStats() {
"UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?",
"user count 'num_repos'", "user count 'num_repos'",
}, },
// Issue.NumComments
{
"SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)",
"UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?",
"issue count 'num_comments'",
},
} }
for i := range checkers { for i := range checkers {
repoStatsCheck(checkers[i]) repoStatsCheck(checkers[i])
@ -1636,25 +1696,21 @@ func WatchRepo(uid, repoId int64, watch bool) (err error) {
return watchRepo(x, uid, repoId, watch) return watchRepo(x, uid, repoId, watch)
} }
func getWatchers(e Engine, rid int64) ([]*Watch, error) { func getWatchers(e Engine, repoID int64) ([]*Watch, error) {
watches := make([]*Watch, 0, 10) watches := make([]*Watch, 0, 10)
err := e.Find(&watches, &Watch{RepoID: rid}) return watches, e.Find(&watches, &Watch{RepoID: repoID})
return watches, err
} }
// GetWatchers returns all watchers of given repository. // GetWatchers returns all watchers of given repository.
func GetWatchers(rid int64) ([]*Watch, error) { func GetWatchers(repoID int64) ([]*Watch, error) {
return getWatchers(x, rid) return getWatchers(x, repoID)
} }
// Repository.GetWatchers returns all users watching given repository. // Repository.GetWatchers returns range of users watching given repository.
func (repo *Repository) GetWatchers(offset int) ([]*User, error) { func (repo *Repository) GetWatchers(page int) ([]*User, error) {
users := make([]*User, 0, 10) users := make([]*User, 0, ItemsPerPage)
offset = (offset - 1) * ItemsPerPage return users, x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).
Where("repo_id=?", repo.ID).Join("LEFT", "watch", "user.id=watch.user_id").Find(&users)
err := x.Limit(ItemsPerPage, offset).Where("repo_id=?", repo.ID).Join("LEFT", "watch", "user.id=watch.user_id").Find(&users)
return users, err
} }
func notifyWatchers(e Engine, act *Action) error { func notifyWatchers(e Engine, act *Action) error {
@ -1734,13 +1790,10 @@ func IsStaring(uid, repoId int64) bool {
return has return has
} }
func (repo *Repository) GetStars(offset int) ([]*User, error) { func (repo *Repository) GetStargazers(page int) ([]*User, error) {
users := make([]*User, 0, 10) users := make([]*User, 0, ItemsPerPage)
offset = (offset - 1) * ItemsPerPage return users, x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).
Where("repo_id=?", repo.ID).Join("LEFT", "star", "user.id=star.uid").Find(&users)
err := x.Limit(ItemsPerPage, offset).Where("repo_id=?", repo.ID).Join("LEFT", "star", "user.id=star.uid").Find(&users)
return users, err
} }
// ___________ __ // ___________ __
@ -1812,9 +1865,6 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit
} }
func (repo *Repository) GetForks() ([]*Repository, error) { func (repo *Repository) GetForks() ([]*Repository, error) {
forks := make([]*Repository, 0, 10) forks := make([]*Repository, 0, repo.NumForks)
return forks, x.Find(&forks, &Repository{ForkID: repo.ID})
err := x.Find(&forks, &Repository{ForkID: repo.ID})
return forks, err
} }

65
models/update.go

@ -10,17 +10,16 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/git"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
) )
type UpdateTask struct { type UpdateTask struct {
Id int64 ID int64 `xorm:"pk autoincr"`
Uuid string `xorm:"index"` UUID string `xorm:"index"`
RefName string RefName string
OldCommitId string OldCommitID string
NewCommitId string NewCommitID string
} }
func AddUpdateTask(task *UpdateTask) error { func AddUpdateTask(task *UpdateTask) error {
@ -28,27 +27,29 @@ func AddUpdateTask(task *UpdateTask) error {
return err return err
} }
func GetUpdateTasksByUuid(uuid string) ([]*UpdateTask, error) { // GetUpdateTaskByUUID returns update task by given UUID.
func GetUpdateTaskByUUID(uuid string) (*UpdateTask, error) {
task := &UpdateTask{ task := &UpdateTask{
Uuid: uuid, UUID: uuid,
} }
tasks := make([]*UpdateTask, 0) has, err := x.Get(task)
err := x.Find(&tasks, task)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has {
return nil, ErrUpdateTaskNotExist{uuid}
} }
return tasks, nil return task, nil
} }
func DelUpdateTasksByUuid(uuid string) error { func DeleteUpdateTaskByUUID(uuid string) error {
_, err := x.Delete(&UpdateTask{Uuid: uuid}) _, err := x.Delete(&UpdateTask{UUID: uuid})
return err return err
} }
func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName string, userId int64) error { func Update(refName, oldCommitID, newCommitID, userName, repoUserName, repoName string, userID int64) error {
isNew := strings.HasPrefix(oldCommitId, "0000000") isNew := strings.HasPrefix(oldCommitID, "0000000")
if isNew && if isNew &&
strings.HasPrefix(newCommitId, "0000000") { strings.HasPrefix(newCommitID, "0000000") {
return fmt.Errorf("old rev and new rev both 000000") return fmt.Errorf("old rev and new rev both 000000")
} }
@ -58,23 +59,23 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
gitUpdate.Dir = f gitUpdate.Dir = f
gitUpdate.Run() gitUpdate.Run()
isDel := strings.HasPrefix(newCommitId, "0000000") isDel := strings.HasPrefix(newCommitID, "0000000")
if isDel { if isDel {
log.GitLogger.Info("del rev", refName, "from", userName+"/"+repoName+".git", "by", userId) log.GitLogger.Info("del rev", refName, "from", userName+"/"+repoName+".git", "by", userID)
return nil return nil
} }
repo, err := git.OpenRepository(f) gitRepo, err := git.OpenRepository(f)
if err != nil { if err != nil {
return fmt.Errorf("runUpdate.Open repoId: %v", err) return fmt.Errorf("runUpdate.Open repoId: %v", err)
} }
ru, err := GetUserByName(repoUserName) user, err := GetUserByName(repoUserName)
if err != nil { if err != nil {
return fmt.Errorf("runUpdate.GetUserByName: %v", err) return fmt.Errorf("runUpdate.GetUserByName: %v", err)
} }
repos, err := GetRepositoryByName(ru.Id, repoName) repo, err := GetRepositoryByName(user.Id, repoName)
if err != nil { if err != nil {
return fmt.Errorf("runUpdate.GetRepositoryByName userId: %v", err) return fmt.Errorf("runUpdate.GetRepositoryByName userId: %v", err)
} }
@ -82,7 +83,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
// Push tags. // Push tags.
if strings.HasPrefix(refName, "refs/tags/") { if strings.HasPrefix(refName, "refs/tags/") {
tagName := git.RefEndName(refName) tagName := git.RefEndName(refName)
tag, err := repo.GetTag(tagName) tag, err := gitRepo.GetTag(tagName)
if err != nil { if err != nil {
log.GitLogger.Fatal(4, "runUpdate.GetTag: %v", err) log.GitLogger.Fatal(4, "runUpdate.GetTag: %v", err)
} }
@ -98,16 +99,16 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
actEmail = cmt.Committer.Email actEmail = cmt.Committer.Email
} }
commit := &base.PushCommits{} commit := &PushCommits{}
if err = CommitRepoAction(userId, ru.Id, userName, actEmail, if err = CommitRepoAction(userID, user.Id, userName, actEmail,
repos.ID, repoUserName, repoName, refName, commit, oldCommitId, newCommitId); err != nil { repo.ID, repoUserName, repoName, refName, commit, oldCommitID, newCommitID); err != nil {
log.GitLogger.Fatal(4, "CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) log.GitLogger.Fatal(4, "CommitRepoAction: %s/%s:%v", repoUserName, repoName, err)
} }
return err return err
} }
newCommit, err := repo.GetCommit(newCommitId) newCommit, err := gitRepo.GetCommit(newCommitID)
if err != nil { if err != nil {
return fmt.Errorf("runUpdate GetCommit of newCommitId: %v", err) return fmt.Errorf("runUpdate GetCommit of newCommitId: %v", err)
} }
@ -117,12 +118,12 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
if isNew { if isNew {
l, err = newCommit.CommitsBefore() l, err = newCommit.CommitsBefore()
if err != nil { if err != nil {
return fmt.Errorf("Find CommitsBefore erro: %v", err) return fmt.Errorf("CommitsBefore: %v", err)
} }
} else { } else {
l, err = newCommit.CommitsBeforeUntil(oldCommitId) l, err = newCommit.CommitsBeforeUntil(oldCommitID)
if err != nil { if err != nil {
return fmt.Errorf("Find CommitsBeforeUntil erro: %v", err) return fmt.Errorf("CommitsBeforeUntil: %v", err)
} }
} }
@ -131,7 +132,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
} }
// Push commits. // Push commits.
commits := make([]*base.PushCommit, 0) commits := make([]*PushCommit, 0)
var actEmail string var actEmail string
for e := l.Front(); e != nil; e = e.Next() { for e := l.Front(); e != nil; e = e.Next() {
commit := e.Value.(*git.Commit) commit := e.Value.(*git.Commit)
@ -139,15 +140,15 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName
actEmail = commit.Committer.Email actEmail = commit.Committer.Email
} }
commits = append(commits, commits = append(commits,
&base.PushCommit{commit.Id.String(), &PushCommit{commit.ID.String(),
commit.Message(), commit.Message(),
commit.Author.Email, commit.Author.Email,
commit.Author.Name, commit.Author.Name,
}) })
} }
if err = CommitRepoAction(userId, ru.Id, userName, actEmail, if err = CommitRepoAction(userID, user.Id, userName, actEmail,
repos.ID, repoUserName, repoName, refName, &base.PushCommits{l.Len(), commits, ""}, oldCommitId, newCommitId); err != nil { repo.ID, repoUserName, repoName, refName, &PushCommits{l.Len(), commits, "", nil}, oldCommitID, newCommitID); err != nil {
return fmt.Errorf("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) return fmt.Errorf("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err)
} }
return nil return nil

49
models/user.go

@ -14,6 +14,7 @@ import (
"image" "image"
"image/jpeg" "image/jpeg"
_ "image/jpeg" _ "image/jpeg"
"image/png"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -71,13 +72,14 @@ type User struct {
Created time.Time `xorm:"CREATED"` Created time.Time `xorm:"CREATED"`
Updated time.Time `xorm:"UPDATED"` Updated time.Time `xorm:"UPDATED"`
// Remember visibility choice for convenience. // Remember visibility choice for convenience, true for private
LastRepoVisibility bool LastRepoVisibility bool
// Permissions. // Permissions.
IsActive bool IsActive bool
IsAdmin bool IsAdmin bool
AllowGitHook bool AllowGitHook bool
AllowImportLocal bool // Allow migrate repository by local path
// Avatar. // Avatar.
Avatar string `xorm:"VARCHAR(2048) NOT NULL"` Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
@ -107,6 +109,22 @@ func (u *User) AfterSet(colName string, _ xorm.Cell) {
} }
} }
// HasForkedRepo checks if user has already forked a repository with given ID.
func (u *User) HasForkedRepo(repoID int64) bool {
_, has := HasForkedRepo(u.Id, repoID)
return has
}
// CanEditGitHook returns true if user can edit Git hooks.
func (u *User) CanEditGitHook() bool {
return u.IsAdmin || u.AllowGitHook
}
// CanImportLocal returns true if user can migrate repository by local path.
func (u *User) CanImportLocal() bool {
return u.IsAdmin || u.AllowImportLocal
}
// EmailAdresses is the list of all email addresses of a user. Can contain the // EmailAdresses is the list of all email addresses of a user. Can contain the
// primary email address, but is not obligatory // primary email address, but is not obligatory
type EmailAddress struct { type EmailAddress struct {
@ -242,14 +260,12 @@ func (u *User) ValidatePassword(passwd string) bool {
// UploadAvatar saves custom avatar for user. // UploadAvatar saves custom avatar for user.
// FIXME: split uploads to different subdirs in case we have massive users. // FIXME: split uploads to different subdirs in case we have massive users.
func (u *User) UploadAvatar(data []byte) error { func (u *User) UploadAvatar(data []byte) error {
u.UseCustomAvatar = true
img, _, err := image.Decode(bytes.NewReader(data)) img, _, err := image.Decode(bytes.NewReader(data))
if err != nil { if err != nil {
return err return fmt.Errorf("Decode: %v", err)
} }
m := resize.Resize(234, 234, img, resize.NearestNeighbor) m := resize.Resize(290, 290, img, resize.NearestNeighbor)
sess := x.NewSession() sess := x.NewSession()
defer sessionRelease(sess) defer sessionRelease(sess)
@ -257,19 +273,20 @@ func (u *User) UploadAvatar(data []byte) error {
return err return err
} }
if _, err = sess.Id(u.Id).AllCols().Update(u); err != nil { u.UseCustomAvatar = true
return err if err = updateUser(sess, u); err != nil {
return fmt.Errorf("updateUser: %v", err)
} }
os.MkdirAll(setting.AvatarUploadPath, os.ModePerm) os.MkdirAll(setting.AvatarUploadPath, os.ModePerm)
fw, err := os.Create(u.CustomAvatarPath()) fw, err := os.Create(u.CustomAvatarPath())
if err != nil { if err != nil {
return err return fmt.Errorf("Create: %v", err)
} }
defer fw.Close() defer fw.Close()
if err = jpeg.Encode(fw, m, nil); err != nil { if err = png.Encode(fw, m); err != nil {
return err return fmt.Errorf("Encode: %v", err)
} }
return sess.Commit() return sess.Commit()
@ -717,9 +734,9 @@ func UserPath(userName string) string {
return filepath.Join(setting.RepoRootPath, strings.ToLower(userName)) return filepath.Join(setting.RepoRootPath, strings.ToLower(userName))
} }
func GetUserByKeyId(keyId int64) (*User, error) { func GetUserByKeyID(keyID int64) (*User, error) {
user := new(User) user := new(User)
has, err := x.Sql("SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?", keyId).Get(user) has, err := x.Sql("SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?", keyID).Get(user)
if err != nil { if err != nil {
return nil, err return nil, err
} else if !has { } else if !has {
@ -980,7 +997,7 @@ func GetUserByEmail(email string) (*User, error) {
return GetUserByID(emailAddress.UID) return GetUserByID(emailAddress.UID)
} }
return nil, ErrUserNotExist{0, "email"} return nil, ErrUserNotExist{0, email}
} }
// SearchUserByName returns given number of users whose name contains keyword. // SearchUserByName returns given number of users whose name contains keyword.

82
models/webhook.go

@ -13,6 +13,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
api "github.com/kiliit/go-gogs-client" api "github.com/kiliit/go-gogs-client"
@ -435,39 +436,65 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err
return nil return nil
} }
type hookQueue struct { // UniqueQueue represents a queue that guarantees only one instance of same ID is in the line.
// Make sure one repository only occur once in the queue. type UniqueQueue struct {
lock sync.Mutex lock sync.Mutex
repoIDs map[int64]bool ids map[string]bool
queue chan int64 queue chan string
} }
func (q *hookQueue) removeRepoID(id int64) { func (q *UniqueQueue) Queue() <-chan string {
return q.queue
}
func NewUniqueQueue(queueLength int) *UniqueQueue {
if queueLength <= 0 {
queueLength = 100
}
return &UniqueQueue{
ids: make(map[string]bool),
queue: make(chan string, queueLength),
}
}
func (q *UniqueQueue) Remove(id interface{}) {
q.lock.Lock() q.lock.Lock()
defer q.lock.Unlock() defer q.lock.Unlock()
delete(q.repoIDs, id) delete(q.ids, com.ToStr(id))
} }
func (q *hookQueue) addRepoID(id int64) { func (q *UniqueQueue) AddFunc(id interface{}, fn func()) {
q.lock.Lock() newid := com.ToStr(id)
if q.repoIDs[id] {
q.lock.Unlock() if q.Exist(id) {
return return
} }
q.repoIDs[id] = true
q.lock.Lock()
q.ids[newid] = true
if fn != nil {
fn()
}
q.lock.Unlock() q.lock.Unlock()
q.queue <- id q.queue <- newid
} }
// AddRepoID adds repository ID to hook delivery queue. func (q *UniqueQueue) Add(id interface{}) {
func (q *hookQueue) AddRepoID(id int64) { q.AddFunc(id, nil)
go q.addRepoID(id) }
func (q *UniqueQueue) Exist(id interface{}) bool {
q.lock.Lock()
defer q.lock.Unlock()
return q.ids[com.ToStr(id)]
} }
var HookQueue *hookQueue var HookQueue = NewUniqueQueue(setting.Webhook.QueueLength)
func deliverHook(t *HookTask) { func (t *HookTask) deliver() {
t.IsDelivered = true t.IsDelivered = true
timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
@ -549,12 +576,13 @@ func deliverHook(t *HookTask) {
} }
// DeliverHooks checks and delivers undelivered hooks. // DeliverHooks checks and delivers undelivered hooks.
// TODO: shoot more hooks at same time.
func DeliverHooks() { func DeliverHooks() {
tasks := make([]*HookTask, 0, 10) tasks := make([]*HookTask, 0, 10)
x.Where("is_delivered=?", false).Iterate(new(HookTask), x.Where("is_delivered=?", false).Iterate(new(HookTask),
func(idx int, bean interface{}) error { func(idx int, bean interface{}) error {
t := bean.(*HookTask) t := bean.(*HookTask)
deliverHook(t) t.deliver()
tasks = append(tasks, t) tasks = append(tasks, t)
return nil return nil
}) })
@ -566,15 +594,10 @@ func DeliverHooks() {
} }
} }
HookQueue = &hookQueue{
lock: sync.Mutex{},
repoIDs: make(map[int64]bool),
queue: make(chan int64, setting.Webhook.QueueLength),
}
// Start listening on new hook requests. // Start listening on new hook requests.
for repoID := range HookQueue.queue { for repoID := range HookQueue.Queue() {
HookQueue.removeRepoID(repoID) log.Trace("DeliverHooks[%v]: processing delivery hooks", repoID)
HookQueue.Remove(repoID)
tasks = make([]*HookTask, 0, 5) tasks = make([]*HookTask, 0, 5)
if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil { if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil {
@ -582,9 +605,10 @@ func DeliverHooks() {
continue continue
} }
for _, t := range tasks { for _, t := range tasks {
deliverHook(t) t.deliver()
if err := UpdateHookTask(t); err != nil { if err := UpdateHookTask(t); err != nil {
log.Error(4, "UpdateHookTask(%d): %v", t.ID, err) log.Error(4, "UpdateHookTask[%d]: %v", t.ID, err)
continue
} }
} }
} }

21
modules/auth/admin.go

@ -24,16 +24,17 @@ func (f *AdminCrateUserForm) Validate(ctx *macaron.Context, errs binding.Errors)
} }
type AdminEditUserForm struct { type AdminEditUserForm struct {
LoginType string `binding:"Required"` LoginType string `binding:"Required"`
LoginName string LoginName string
FullName string `binding:"MaxSize(100)"` FullName string `binding:"MaxSize(100)"`
Email string `binding:"Required;Email;MaxSize(254)"` Email string `binding:"Required;Email;MaxSize(254)"`
Password string `binding:"MaxSize(255)"` Password string `binding:"MaxSize(255)"`
Website string `binding:"MaxSize(50)"` Website string `binding:"MaxSize(50)"`
Location string `binding:"MaxSize(50)"` Location string `binding:"MaxSize(50)"`
Active bool Active bool
Admin bool Admin bool
AllowGitHook bool AllowGitHook bool
AllowImportLocal bool
} }
func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { func (f *AdminEditUserForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

14
modules/auth/auth.go

@ -181,7 +181,7 @@ func AssignForm(form interface{}, data map[string]interface{}) {
} }
} }
func getSize(field reflect.StructField, prefix string) string { func getRuleBody(field reflect.StructField, prefix string) string {
for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { for _, rule := range strings.Split(field.Tag.Get("binding"), ";") {
if strings.HasPrefix(rule, prefix) { if strings.HasPrefix(rule, prefix) {
return rule[len(prefix) : len(rule)-1] return rule[len(prefix) : len(rule)-1]
@ -191,15 +191,19 @@ func getSize(field reflect.StructField, prefix string) string {
} }
func GetSize(field reflect.StructField) string { func GetSize(field reflect.StructField) string {
return getSize(field, "Size(") return getRuleBody(field, "Size(")
} }
func GetMinSize(field reflect.StructField) string { func GetMinSize(field reflect.StructField) string {
return getSize(field, "MinSize(") return getRuleBody(field, "MinSize(")
} }
func GetMaxSize(field reflect.StructField) string { func GetMaxSize(field reflect.StructField) string {
return getSize(field, "MaxSize(") return getRuleBody(field, "MaxSize(")
}
func GetInclude(field reflect.StructField) string {
return getRuleBody(field, "Include(")
} }
// FIXME: struct contains a struct // FIXME: struct contains a struct
@ -260,6 +264,8 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro
data["ErrorMsg"] = trName + l.Tr("form.email_error") data["ErrorMsg"] = trName + l.Tr("form.email_error")
case binding.ERR_URL: case binding.ERR_URL:
data["ErrorMsg"] = trName + l.Tr("form.url_error") data["ErrorMsg"] = trName + l.Tr("form.url_error")
case binding.ERR_INCLUDE:
data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
default: default:
data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification
} }

44
modules/auth/ldap/ldap.go

@ -9,6 +9,7 @@ package ldap
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"strings"
"github.com/gogits/gogs/modules/ldap" "github.com/gogits/gogs/modules/ldap"
"github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/log"
@ -33,6 +34,28 @@ type Source struct {
Enabled bool // if this source is disabled Enabled bool // if this source is disabled
} }
func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4515
badCharacters := "\x00()*\\"
if strings.ContainsAny(username, badCharacters) {
log.Debug("'%s' contains invalid query characters. Aborting.", username)
return "", false
}
return fmt.Sprintf(ls.Filter, username), true
}
func (ls *Source) sanitizedUserDN(username string) (string, bool) {
// See http://tools.ietf.org/search/rfc4514: "special characters"
badCharacters := "\x00()*\\,='\"#+;<> "
if strings.ContainsAny(username, badCharacters) {
log.Debug("'%s' contains invalid DN characters. Aborting.", username)
return "", false
}
return fmt.Sprintf(ls.UserDN, username), true
}
func (ls *Source) FindUserDN(name string) (string, bool) { func (ls *Source) FindUserDN(name string) (string, bool) {
l, err := ldapDial(ls) l, err := ldapDial(ls)
if err != nil { if err != nil {
@ -55,7 +78,11 @@ func (ls *Source) FindUserDN(name string) (string, bool) {
} }
// A search for the user. // A search for the user.
userFilter := fmt.Sprintf(ls.Filter, name) userFilter, ok := ls.sanitizedUserQuery(name)
if !ok {
return "", false
}
log.Trace("Searching using filter %s", userFilter) log.Trace("Searching using filter %s", userFilter)
search := ldap.NewSearchRequest( search := ldap.NewSearchRequest(
ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, ls.UserBase, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0,
@ -85,7 +112,12 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
var userDN string var userDN string
if directBind { if directBind {
log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN) log.Trace("LDAP will bind directly via UserDN template: %s", ls.UserDN)
userDN = fmt.Sprintf(ls.UserDN, name)
var ok bool
userDN, ok = ls.sanitizedUserDN(name)
if !ok {
return "", "", "", false, false
}
} else { } else {
log.Trace("LDAP will use BindDN.") log.Trace("LDAP will use BindDN.")
@ -98,7 +130,7 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
l, err := ldapDial(ls) l, err := ldapDial(ls)
if err != nil { if err != nil {
log.Error(4, "LDAP Connect error, %s:%v", ls.Host, err) log.Error(4, "LDAP Connect error (%s): %v", ls.Host, err)
ls.Enabled = false ls.Enabled = false
return "", "", "", false, false return "", "", "", false, false
} }
@ -112,7 +144,11 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) (string, str
} }
log.Trace("Bound successfully with userDN: %s", userDN) log.Trace("Bound successfully with userDN: %s", userDN)
userFilter := fmt.Sprintf(ls.Filter, name) userFilter, ok := ls.sanitizedUserQuery(name)
if !ok {
return "", "", "", false, false
}
search := ldap.NewSearchRequest( search := ldap.NewSearchRequest(
userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter, userDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, userFilter,
[]string{ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}, []string{ls.AttributeName, ls.AttributeSurname, ls.AttributeMail},

48
modules/auth/repo_form.go

@ -5,8 +5,14 @@
package auth package auth
import ( import (
"net/url"
"strings"
"github.com/Unknwon/com"
"github.com/go-macaron/binding" "github.com/go-macaron/binding"
"gopkg.in/macaron.v1" "gopkg.in/macaron.v1"
"github.com/gogits/gogs/models"
) )
// _______________________________________ _________.______________________ _______________.___. // _______________________________________ _________.______________________ _______________.___.
@ -37,8 +43,8 @@ type MigrateRepoForm struct {
AuthPassword string `json:"auth_password"` AuthPassword string `json:"auth_password"`
Uid int64 `json:"uid" binding:"Required"` Uid int64 `json:"uid" binding:"Required"`
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
Private bool `json:"mirror"` Mirror bool `json:"mirror"`
Mirror bool `json:"private"` Private bool `json:"private"`
Description string `json:"description" binding:"MaxSize(255)"` Description string `json:"description" binding:"MaxSize(255)"`
} }
@ -46,6 +52,34 @@ func (f *MigrateRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
return validate(errs, ctx.Data, f, ctx.Locale) return validate(errs, ctx.Data, f, ctx.Locale)
} }
// ParseRemoteAddr checks if given remote address is valid,
// and returns composed URL with needed username and passowrd.
// It also checks if given user has permission when remote address
// is actually a local path.
func (f MigrateRepoForm) ParseRemoteAddr(user *models.User) (string, error) {
remoteAddr := f.CloneAddr
// Remote address can be HTTP/HTTPS/Git URL or local path.
if strings.HasPrefix(remoteAddr, "http://") ||
strings.HasPrefix(remoteAddr, "https://") ||
strings.HasPrefix(remoteAddr, "git://") {
u, err := url.Parse(remoteAddr)
if err != nil {
return "", models.ErrInvalidCloneAddr{IsURLError: true}
}
if len(f.AuthUsername)+len(f.AuthPassword) > 0 {
u.User = url.UserPassword(f.AuthUsername, f.AuthPassword)
}
remoteAddr = u.String()
} else if !user.CanImportLocal() {
return "", models.ErrInvalidCloneAddr{IsPermissionDenied: true}
} else if !com.IsDir(remoteAddr) {
return "", models.ErrInvalidCloneAddr{IsInvalidPath: true}
}
return remoteAddr, nil
}
type RepoSettingForm struct { type RepoSettingForm struct {
RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"` RepoName string `binding:"Required;AlphaDashDot;MaxSize(100)"`
Description string `binding:"MaxSize(255)"` Description string `binding:"MaxSize(255)"`
@ -181,12 +215,12 @@ func (f *CreateLabelForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
// \/ \/ \/ \/ \/ \/ // \/ \/ \/ \/ \/ \/
type NewReleaseForm struct { type NewReleaseForm struct {
TagName string `form:"tag_name" binding:"Required"` TagName string `binding:"Required"`
Target string `form:"tag_target" binding:"Required"` Target string `form:"tag_target" binding:"Required"`
Title string `form:"title" binding:"Required"` Title string `binding:"Required"`
Content string `form:"content" binding:"Required"` Content string
Draft string `form:"draft"` Draft string
Prerelease bool `form:"prerelease"` Prerelease bool
} }
func (f *NewReleaseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { func (f *NewReleaseForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

4
modules/auth/user_form.go

@ -30,7 +30,7 @@ type InstallForm struct {
SMTPHost string SMTPHost string
SMTPFrom string SMTPFrom string
SMTPEmail string `binding:"OmitEmpty;Email;MaxSize(50)" locale:"install.mailer_user"` SMTPEmail string `binding:"OmitEmpty;Email;MaxSize(254)" locale:"install.mailer_user"`
SMTPPasswd string SMTPPasswd string
RegisterConfirm bool RegisterConfirm bool
MailNotify bool MailNotify bool
@ -44,7 +44,7 @@ type InstallForm struct {
AdminName string `binding:"OmitEmpty;AlphaDashDot;MaxSize(30)" locale:"install.admin_name"` AdminName string `binding:"OmitEmpty;AlphaDashDot;MaxSize(30)" locale:"install.admin_name"`
AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"` AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"`
AdminConfirmPasswd string AdminConfirmPasswd string
AdminEmail string `binding:"OmitEmpty;Email;MaxSize(50)" locale:"install.admin_email"` AdminEmail string `binding:"OmitEmpty;MinSize(3);MaxSize(254);Include(@)" locale:"install.admin_email"`
} }
func (f *InstallForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { func (f *InstallForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {

8
modules/avatar/avatar.go

@ -39,6 +39,8 @@ import (
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
) )
//FIXME: remove cache module
var gravatarSource string var gravatarSource string
func UpdateGravatarSource() { func UpdateGravatarSource() {
@ -102,7 +104,7 @@ func New(hash string, cacheDir string) *Avatar {
expireDuration: time.Minute * 10, expireDuration: time.Minute * 10,
reqParams: url.Values{ reqParams: url.Values{
"d": {"retro"}, "d": {"retro"},
"size": {"200"}, "size": {"290"},
"r": {"pg"}}.Encode(), "r": {"pg"}}.Encode(),
imagePath: filepath.Join(cacheDir, hash+".image"), //maybe png or jpeg imagePath: filepath.Join(cacheDir, hash+".image"), //maybe png or jpeg
} }
@ -153,7 +155,7 @@ func (this *Avatar) Encode(wr io.Writer, size int) (err error) {
if img, err = decodeImageFile(imgPath); err != nil { if img, err = decodeImageFile(imgPath); err != nil {
return return
} }
m := resize.Resize(uint(size), 0, img, resize.NearestNeighbor) m := resize.Resize(uint(size), 0, img, resize.Lanczos3)
return jpeg.Encode(wr, m, nil) return jpeg.Encode(wr, m, nil)
} }
@ -192,7 +194,7 @@ func (this *service) mustInt(r *http.Request, defaultValue int, keys ...string)
func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path urlPath := r.URL.Path
hash := urlPath[strings.LastIndex(urlPath, "/")+1:] hash := urlPath[strings.LastIndex(urlPath, "/")+1:]
size := this.mustInt(r, 80, "s", "size") // default size = 80*80 size := this.mustInt(r, 290, "s", "size") // default size = 290*290
avatar := New(hash, this.cacheDir) avatar := New(hash, this.cacheDir)
avatar.AlterImage = this.altImage avatar.AlterImage = this.altImage

19
modules/base/base.go

@ -4,6 +4,12 @@
package base package base
import (
"os"
"os/exec"
"path/filepath"
)
const DOC_URL = "https://github.com/kiliit/go-gogs-client/wiki" const DOC_URL = "https://github.com/kiliit/go-gogs-client/wiki"
type ( type (
@ -11,3 +17,16 @@ type (
) )
var GoGetMetas = make(map[string]bool) var GoGetMetas = make(map[string]bool)
// ExecPath returns the executable path.
func ExecPath() (string, error) {
file, err := exec.LookPath(os.Args[0])
if err != nil {
return "", err
}
p, err := filepath.Abs(file)
if err != nil {
return "", err
}
return p, nil
}

24
modules/base/markdown.go

@ -100,11 +100,19 @@ func (options *CustomRender) Link(out *bytes.Buffer, link []byte, title []byte,
} }
func (options *CustomRender) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { func (options *CustomRender) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
prefix := strings.Replace(options.urlPrefix, "/src/", "/raw/", 1)
if len(link) > 0 && !isLink(link) { if len(link) > 0 && !isLink(link) {
link = []byte(path.Join(strings.Replace(options.urlPrefix, "/src/", "/raw/", 1), string(link))) if link[0] != '/' {
prefix += "/"
}
link = []byte(prefix + string(link))
} }
out.WriteString(`<a href="`)
out.Write(link)
out.WriteString(`">`)
options.Renderer.Image(out, link, title, alt) options.Renderer.Image(out, link, title, alt)
out.WriteString("</a>")
} }
var ( var (
@ -159,7 +167,21 @@ func RenderSha1CurrentPattern(rawBytes []byte, urlPrefix string) []byte {
return rawBytes return rawBytes
} }
func cutoutVerbosePrefix(prefix string) string {
count := 0
for i := 0; i < len(prefix); i++ {
if prefix[i] == '/' {
count++
}
if count >= 3 {
return prefix[:i]
}
}
return prefix
}
func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string) []byte { func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string) []byte {
urlPrefix = cutoutVerbosePrefix(urlPrefix)
ms := issueIndexPattern.FindAll(rawBytes, -1) ms := issueIndexPattern.FindAll(rawBytes, -1)
for _, m := range ms { for _, m := range ms {
var space string var space string

18
modules/base/tool.go

@ -23,6 +23,8 @@ import (
"github.com/Unknwon/i18n" "github.com/Unknwon/i18n"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/gogits/chardet"
"github.com/gogits/gogs/modules/avatar" "github.com/gogits/gogs/modules/avatar"
"github.com/gogits/gogs/modules/setting" "github.com/gogits/gogs/modules/setting"
) )
@ -43,6 +45,22 @@ func EncodeSha1(str string) string {
return hex.EncodeToString(h.Sum(nil)) return hex.EncodeToString(h.Sum(nil))
} }
func ShortSha(sha1 string) string {
if len(sha1) == 40 {
return sha1[:10]
}
return sha1
}
func DetectEncoding(content []byte) (string, error) {
detector := chardet.NewTextDetector()
result, err := detector.DetectBest(content)
if result.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
return setting.Repository.AnsiCharset, err
}
return result.Charset, err
}
func BasicAuthDecode(encoded string) (string, string, error) { func BasicAuthDecode(encoded string) (string, string, error) {
s, err := base64.StdEncoding.DecodeString(encoded) s, err := base64.StdEncoding.DecodeString(encoded)
if err != nil { if err != nil {

64
modules/bindata/bindata.go

File diff suppressed because one or more lines are too long

2
modules/cron/parser_test.go

@ -111,7 +111,7 @@ func TestSpecSchedule(t *testing.T) {
t.Error(err) t.Error(err)
} }
if !reflect.DeepEqual(actual, c.expected) { if !reflect.DeepEqual(actual, c.expected) {
t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual) t.Errorf("%s => (expected) %v != %v (actual)", c.expr, c.expected, actual)
} }
} }
} }

615
modules/crypto/ssh/agent/client.go

@ -1,615 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package agent implements a client to an ssh-agent daemon.
References:
[PROTOCOL.agent]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent?rev=HEAD
*/
package agent
import (
"bytes"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"sync"
"github.com/gogits/gogs/modules/crypto/ssh"
)
// Agent represents the capabilities of an ssh-agent.
type Agent interface {
// List returns the identities known to the agent.
List() ([]*Key, error)
// Sign has the agent sign the data using a protocol 2 key as defined
// in [PROTOCOL.agent] section 2.6.2.
Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error)
// Add adds a private key to the agent.
Add(key AddedKey) error
// Remove removes all identities with the given public key.
Remove(key ssh.PublicKey) error
// RemoveAll removes all identities.
RemoveAll() error
// Lock locks the agent. Sign and Remove will fail, and List will empty an empty list.
Lock(passphrase []byte) error
// Unlock undoes the effect of Lock
Unlock(passphrase []byte) error
// Signers returns signers for all the known keys.
Signers() ([]ssh.Signer, error)
}
// AddedKey describes an SSH key to be added to an Agent.
type AddedKey struct {
// PrivateKey must be a *rsa.PrivateKey, *dsa.PrivateKey or
// *ecdsa.PrivateKey, which will be inserted into the agent.
PrivateKey interface{}
// Certificate, if not nil, is communicated to the agent and will be
// stored with the key.
Certificate *ssh.Certificate
// Comment is an optional, free-form string.
Comment string
// LifetimeSecs, if not zero, is the number of seconds that the
// agent will store the key for.
LifetimeSecs uint32
// ConfirmBeforeUse, if true, requests that the agent confirm with the
// user before each use of this key.
ConfirmBeforeUse bool
}
// See [PROTOCOL.agent], section 3.
const (
agentRequestV1Identities = 1
// 3.2 Requests from client to agent for protocol 2 key operations
agentAddIdentity = 17
agentRemoveIdentity = 18
agentRemoveAllIdentities = 19
agentAddIdConstrained = 25
// 3.3 Key-type independent requests from client to agent
agentAddSmartcardKey = 20
agentRemoveSmartcardKey = 21
agentLock = 22
agentUnlock = 23
agentAddSmartcardKeyConstrained = 26
// 3.7 Key constraint identifiers
agentConstrainLifetime = 1
agentConstrainConfirm = 2
)
// maxAgentResponseBytes is the maximum agent reply size that is accepted. This
// is a sanity check, not a limit in the spec.
const maxAgentResponseBytes = 16 << 20
// Agent messages:
// These structures mirror the wire format of the corresponding ssh agent
// messages found in [PROTOCOL.agent].
// 3.4 Generic replies from agent to client
const agentFailure = 5
type failureAgentMsg struct{}
const agentSuccess = 6
type successAgentMsg struct{}
// See [PROTOCOL.agent], section 2.5.2.
const agentRequestIdentities = 11
type requestIdentitiesAgentMsg struct{}
// See [PROTOCOL.agent], section 2.5.2.
const agentIdentitiesAnswer = 12
type identitiesAnswerAgentMsg struct {
NumKeys uint32 `sshtype:"12"`
Keys []byte `ssh:"rest"`
}
// See [PROTOCOL.agent], section 2.6.2.
const agentSignRequest = 13
type signRequestAgentMsg struct {
KeyBlob []byte `sshtype:"13"`
Data []byte
Flags uint32
}
// See [PROTOCOL.agent], section 2.6.2.
// 3.6 Replies from agent to client for protocol 2 key operations
const agentSignResponse = 14
type signResponseAgentMsg struct {
SigBlob []byte `sshtype:"14"`
}
type publicKey struct {
Format string
Rest []byte `ssh:"rest"`
}
// Key represents a protocol 2 public key as defined in
// [PROTOCOL.agent], section 2.5.2.
type Key struct {
Format string
Blob []byte
Comment string
}
func clientErr(err error) error {
return fmt.Errorf("agent: client error: %v", err)
}
// String returns the storage form of an agent key with the format, base64
// encoded serialized key, and the comment if it is not empty.
func (k *Key) String() string {
s := string(k.Format) + " " + base64.StdEncoding.EncodeToString(k.Blob)
if k.Comment != "" {
s += " " + k.Comment
}
return s
}
// Type returns the public key type.
func (k *Key) Type() string {
return k.Format
}
// Marshal returns key blob to satisfy the ssh.PublicKey interface.
func (k *Key) Marshal() []byte {
return k.Blob
}
// Verify satisfies the ssh.PublicKey interface, but is not
// implemented for agent keys.
func (k *Key) Verify(data []byte, sig *ssh.Signature) error {
return errors.New("agent: agent key does not know how to verify")
}
type wireKey struct {
Format string
Rest []byte `ssh:"rest"`
}
func parseKey(in []byte) (out *Key, rest []byte, err error) {
var record struct {
Blob []byte
Comment string
Rest []byte `ssh:"rest"`
}
if err := ssh.Unmarshal(in, &record); err != nil {
return nil, nil, err
}
var wk wireKey
if err := ssh.Unmarshal(record.Blob, &wk); err != nil {
return nil, nil, err
}
return &Key{
Format: wk.Format,
Blob: record.Blob,
Comment: record.Comment,
}, record.Rest, nil
}
// client is a client for an ssh-agent process.
type client struct {
// conn is typically a *net.UnixConn
conn io.ReadWriter
// mu is used to prevent concurrent access to the agent
mu sync.Mutex
}
// NewClient returns an Agent that talks to an ssh-agent process over
// the given connection.
func NewClient(rw io.ReadWriter) Agent {
return &client{conn: rw}
}
// call sends an RPC to the agent. On success, the reply is
// unmarshaled into reply and replyType is set to the first byte of
// the reply, which contains the type of the message.
func (c *client) call(req []byte) (reply interface{}, err error) {
c.mu.Lock()
defer c.mu.Unlock()
msg := make([]byte, 4+len(req))
binary.BigEndian.PutUint32(msg, uint32(len(req)))
copy(msg[4:], req)
if _, err = c.conn.Write(msg); err != nil {
return nil, clientErr(err)
}
var respSizeBuf [4]byte
if _, err = io.ReadFull(c.conn, respSizeBuf[:]); err != nil {
return nil, clientErr(err)
}
respSize := binary.BigEndian.Uint32(respSizeBuf[:])
if respSize > maxAgentResponseBytes {
return nil, clientErr(err)
}
buf := make([]byte, respSize)
if _, err = io.ReadFull(c.conn, buf); err != nil {
return nil, clientErr(err)
}
reply, err = unmarshal(buf)
if err != nil {
return nil, clientErr(err)
}
return reply, err
}
func (c *client) simpleCall(req []byte) error {
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
func (c *client) RemoveAll() error {
return c.simpleCall([]byte{agentRemoveAllIdentities})
}
func (c *client) Remove(key ssh.PublicKey) error {
req := ssh.Marshal(&agentRemoveIdentityMsg{
KeyBlob: key.Marshal(),
})
return c.simpleCall(req)
}
func (c *client) Lock(passphrase []byte) error {
req := ssh.Marshal(&agentLockMsg{
Passphrase: passphrase,
})
return c.simpleCall(req)
}
func (c *client) Unlock(passphrase []byte) error {
req := ssh.Marshal(&agentUnlockMsg{
Passphrase: passphrase,
})
return c.simpleCall(req)
}
// List returns the identities known to the agent.
func (c *client) List() ([]*Key, error) {
// see [PROTOCOL.agent] section 2.5.2.
req := []byte{agentRequestIdentities}
msg, err := c.call(req)
if err != nil {
return nil, err
}
switch msg := msg.(type) {
case *identitiesAnswerAgentMsg:
if msg.NumKeys > maxAgentResponseBytes/8 {
return nil, errors.New("agent: too many keys in agent reply")
}
keys := make([]*Key, msg.NumKeys)
data := msg.Keys
for i := uint32(0); i < msg.NumKeys; i++ {
var key *Key
var err error
if key, data, err = parseKey(data); err != nil {
return nil, err
}
keys[i] = key
}
return keys, nil
case *failureAgentMsg:
return nil, errors.New("agent: failed to list keys")
}
panic("unreachable")
}
// Sign has the agent sign the data using a protocol 2 key as defined
// in [PROTOCOL.agent] section 2.6.2.
func (c *client) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
req := ssh.Marshal(signRequestAgentMsg{
KeyBlob: key.Marshal(),
Data: data,
})
msg, err := c.call(req)
if err != nil {
return nil, err
}
switch msg := msg.(type) {
case *signResponseAgentMsg:
var sig ssh.Signature
if err := ssh.Unmarshal(msg.SigBlob, &sig); err != nil {
return nil, err
}
return &sig, nil
case *failureAgentMsg:
return nil, errors.New("agent: failed to sign challenge")
}
panic("unreachable")
}
// unmarshal parses an agent message in packet, returning the parsed
// form and the message type of packet.
func unmarshal(packet []byte) (interface{}, error) {
if len(packet) < 1 {
return nil, errors.New("agent: empty packet")
}
var msg interface{}
switch packet[0] {
case agentFailure:
return new(failureAgentMsg), nil
case agentSuccess:
return new(successAgentMsg), nil
case agentIdentitiesAnswer:
msg = new(identitiesAnswerAgentMsg)
case agentSignResponse:
msg = new(signResponseAgentMsg)
default:
return nil, fmt.Errorf("agent: unknown type tag %d", packet[0])
}
if err := ssh.Unmarshal(packet, msg); err != nil {
return nil, err
}
return msg, nil
}
type rsaKeyMsg struct {
Type string `sshtype:"17"`
N *big.Int
E *big.Int
D *big.Int
Iqmp *big.Int // IQMP = Inverse Q Mod P
P *big.Int
Q *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type dsaKeyMsg struct {
Type string `sshtype:"17"`
P *big.Int
Q *big.Int
G *big.Int
Y *big.Int
X *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type ecdsaKeyMsg struct {
Type string `sshtype:"17"`
Curve string
KeyBytes []byte
D *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
// Insert adds a private key to the agent.
func (c *client) insertKey(s interface{}, comment string, constraints []byte) error {
var req []byte
switch k := s.(type) {
case *rsa.PrivateKey:
if len(k.Primes) != 2 {
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
}
k.Precompute()
req = ssh.Marshal(rsaKeyMsg{
Type: ssh.KeyAlgoRSA,
N: k.N,
E: big.NewInt(int64(k.E)),
D: k.D,
Iqmp: k.Precomputed.Qinv,
P: k.Primes[0],
Q: k.Primes[1],
Comments: comment,
Constraints: constraints,
})
case *dsa.PrivateKey:
req = ssh.Marshal(dsaKeyMsg{
Type: ssh.KeyAlgoDSA,
P: k.P,
Q: k.Q,
G: k.G,
Y: k.Y,
X: k.X,
Comments: comment,
Constraints: constraints,
})
case *ecdsa.PrivateKey:
nistID := fmt.Sprintf("nistp%d", k.Params().BitSize)
req = ssh.Marshal(ecdsaKeyMsg{
Type: "ecdsa-sha2-" + nistID,
Curve: nistID,
KeyBytes: elliptic.Marshal(k.Curve, k.X, k.Y),
D: k.D,
Comments: comment,
Constraints: constraints,
})
default:
return fmt.Errorf("agent: unsupported key type %T", s)
}
// if constraints are present then the message type needs to be changed.
if len(constraints) != 0 {
req[0] = agentAddIdConstrained
}
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
type rsaCertMsg struct {
Type string `sshtype:"17"`
CertBytes []byte
D *big.Int
Iqmp *big.Int // IQMP = Inverse Q Mod P
P *big.Int
Q *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type dsaCertMsg struct {
Type string `sshtype:"17"`
CertBytes []byte
X *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type ecdsaCertMsg struct {
Type string `sshtype:"17"`
CertBytes []byte
D *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
// Insert adds a private key to the agent. If a certificate is given,
// that certificate is added instead as public key.
func (c *client) Add(key AddedKey) error {
var constraints []byte
if secs := key.LifetimeSecs; secs != 0 {
constraints = append(constraints, agentConstrainLifetime)
var secsBytes [4]byte
binary.BigEndian.PutUint32(secsBytes[:], secs)
constraints = append(constraints, secsBytes[:]...)
}
if key.ConfirmBeforeUse {
constraints = append(constraints, agentConstrainConfirm)
}
if cert := key.Certificate; cert == nil {
return c.insertKey(key.PrivateKey, key.Comment, constraints)
} else {
return c.insertCert(key.PrivateKey, cert, key.Comment, constraints)
}
}
func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string, constraints []byte) error {
var req []byte
switch k := s.(type) {
case *rsa.PrivateKey:
if len(k.Primes) != 2 {
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
}
k.Precompute()
req = ssh.Marshal(rsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
D: k.D,
Iqmp: k.Precomputed.Qinv,
P: k.Primes[0],
Q: k.Primes[1],
Comments: comment,
Constraints: constraints,
})
case *dsa.PrivateKey:
req = ssh.Marshal(dsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
X: k.X,
Comments: comment,
})
case *ecdsa.PrivateKey:
req = ssh.Marshal(ecdsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
D: k.D,
Comments: comment,
})
default:
return fmt.Errorf("agent: unsupported key type %T", s)
}
// if constraints are present then the message type needs to be changed.
if len(constraints) != 0 {
req[0] = agentAddIdConstrained
}
signer, err := ssh.NewSignerFromKey(s)
if err != nil {
return err
}
if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 {
return errors.New("agent: signer and cert have different public key")
}
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
// Signers provides a callback for client authentication.
func (c *client) Signers() ([]ssh.Signer, error) {
keys, err := c.List()
if err != nil {
return nil, err
}
var result []ssh.Signer
for _, k := range keys {
result = append(result, &agentKeyringSigner{c, k})
}
return result, nil
}
type agentKeyringSigner struct {
agent *client
pub ssh.PublicKey
}
func (s *agentKeyringSigner) PublicKey() ssh.PublicKey {
return s.pub
}
func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
// The agent has its own entropy source, so the rand argument is ignored.
return s.agent.Sign(s.pub, data)
}

287
modules/crypto/ssh/agent/client_test.go

@ -1,287 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"bytes"
"crypto/rand"
"errors"
"net"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"github.com/gogits/gogs/modules/crypto/ssh"
)
// startAgent executes ssh-agent, and returns a Agent interface to it.
func startAgent(t *testing.T) (client Agent, socket string, cleanup func()) {
if testing.Short() {
// ssh-agent is not always available, and the key
// types supported vary by platform.
t.Skip("skipping test due to -short")
}
bin, err := exec.LookPath("ssh-agent")
if err != nil {
t.Skip("could not find ssh-agent")
}
cmd := exec.Command(bin, "-s")
out, err := cmd.Output()
if err != nil {
t.Fatalf("cmd.Output: %v", err)
}
/* Output looks like:
SSH_AUTH_SOCK=/tmp/ssh-P65gpcqArqvH/agent.15541; export SSH_AUTH_SOCK;
SSH_AGENT_PID=15542; export SSH_AGENT_PID;
echo Agent pid 15542;
*/
fields := bytes.Split(out, []byte(";"))
line := bytes.SplitN(fields[0], []byte("="), 2)
line[0] = bytes.TrimLeft(line[0], "\n")
if string(line[0]) != "SSH_AUTH_SOCK" {
t.Fatalf("could not find key SSH_AUTH_SOCK in %q", fields[0])
}
socket = string(line[1])
line = bytes.SplitN(fields[2], []byte("="), 2)
line[0] = bytes.TrimLeft(line[0], "\n")
if string(line[0]) != "SSH_AGENT_PID" {
t.Fatalf("could not find key SSH_AGENT_PID in %q", fields[2])
}
pidStr := line[1]
pid, err := strconv.Atoi(string(pidStr))
if err != nil {
t.Fatalf("Atoi(%q): %v", pidStr, err)
}
conn, err := net.Dial("unix", string(socket))
if err != nil {
t.Fatalf("net.Dial: %v", err)
}
ac := NewClient(conn)
return ac, socket, func() {
proc, _ := os.FindProcess(pid)
if proc != nil {
proc.Kill()
}
conn.Close()
os.RemoveAll(filepath.Dir(socket))
}
}
func testAgent(t *testing.T, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
agent, _, cleanup := startAgent(t)
defer cleanup()
testAgentInterface(t, agent, key, cert, lifetimeSecs)
}
func testAgentInterface(t *testing.T, agent Agent, key interface{}, cert *ssh.Certificate, lifetimeSecs uint32) {
signer, err := ssh.NewSignerFromKey(key)
if err != nil {
t.Fatalf("NewSignerFromKey(%T): %v", key, err)
}
// The agent should start up empty.
if keys, err := agent.List(); err != nil {
t.Fatalf("RequestIdentities: %v", err)
} else if len(keys) > 0 {
t.Fatalf("got %d keys, want 0: %v", len(keys), keys)
}
// Attempt to insert the key, with certificate if specified.
var pubKey ssh.PublicKey
if cert != nil {
err = agent.Add(AddedKey{
PrivateKey: key,
Certificate: cert,
Comment: "comment",
LifetimeSecs: lifetimeSecs,
})
pubKey = cert
} else {
err = agent.Add(AddedKey{PrivateKey: key, Comment: "comment", LifetimeSecs: lifetimeSecs})
pubKey = signer.PublicKey()
}
if err != nil {
t.Fatalf("insert(%T): %v", key, err)
}
// Did the key get inserted successfully?
if keys, err := agent.List(); err != nil {
t.Fatalf("List: %v", err)
} else if len(keys) != 1 {
t.Fatalf("got %v, want 1 key", keys)
} else if keys[0].Comment != "comment" {
t.Fatalf("key comment: got %v, want %v", keys[0].Comment, "comment")
} else if !bytes.Equal(keys[0].Blob, pubKey.Marshal()) {
t.Fatalf("key mismatch")
}
// Can the agent make a valid signature?
data := []byte("hello")
sig, err := agent.Sign(pubKey, data)
if err != nil {
t.Fatalf("Sign(%s): %v", pubKey.Type(), err)
}
if err := pubKey.Verify(data, sig); err != nil {
t.Fatalf("Verify(%s): %v", pubKey.Type(), err)
}
}
func TestAgent(t *testing.T) {
for _, keyType := range []string{"rsa", "dsa", "ecdsa"} {
testAgent(t, testPrivateKeys[keyType], nil, 0)
}
}
func TestCert(t *testing.T) {
cert := &ssh.Certificate{
Key: testPublicKeys["rsa"],
ValidBefore: ssh.CertTimeInfinity,
CertType: ssh.UserCert,
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
testAgent(t, testPrivateKeys["rsa"], cert, 0)
}
func TestConstraints(t *testing.T) {
testAgent(t, testPrivateKeys["rsa"], nil, 3600 /* lifetime in seconds */)
}
// netPipe is analogous to net.Pipe, but it uses a real net.Conn, and
// therefore is buffered (net.Pipe deadlocks if both sides start with
// a write.)
func netPipe() (net.Conn, net.Conn, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, nil, err
}
defer listener.Close()
c1, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
return nil, nil, err
}
c2, err := listener.Accept()
if err != nil {
c1.Close()
return nil, nil, err
}
return c1, c2, nil
}
func TestAuth(t *testing.T) {
a, b, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer a.Close()
defer b.Close()
agent, _, cleanup := startAgent(t)
defer cleanup()
if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"], Comment: "comment"}); err != nil {
t.Errorf("Add: %v", err)
}
serverConf := ssh.ServerConfig{}
serverConf.AddHostKey(testSigners["rsa"])
serverConf.PublicKeyCallback = func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, errors.New("pubkey rejected")
}
go func() {
conn, _, _, err := ssh.NewServerConn(a, &serverConf)
if err != nil {
t.Fatalf("Server: %v", err)
}
conn.Close()
}()
conf := ssh.ClientConfig{}
conf.Auth = append(conf.Auth, ssh.PublicKeysCallback(agent.Signers))
conn, _, _, err := ssh.NewClientConn(b, "", &conf)
if err != nil {
t.Fatalf("NewClientConn: %v", err)
}
conn.Close()
}
func TestLockClient(t *testing.T) {
agent, _, cleanup := startAgent(t)
defer cleanup()
testLockAgent(agent, t)
}
func testLockAgent(agent Agent, t *testing.T) {
if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["rsa"], Comment: "comment 1"}); err != nil {
t.Errorf("Add: %v", err)
}
if err := agent.Add(AddedKey{PrivateKey: testPrivateKeys["dsa"], Comment: "comment dsa"}); err != nil {
t.Errorf("Add: %v", err)
}
if keys, err := agent.List(); err != nil {
t.Errorf("List: %v", err)
} else if len(keys) != 2 {
t.Errorf("Want 2 keys, got %v", keys)
}
passphrase := []byte("secret")
if err := agent.Lock(passphrase); err != nil {
t.Errorf("Lock: %v", err)
}
if keys, err := agent.List(); err != nil {
t.Errorf("List: %v", err)
} else if len(keys) != 0 {
t.Errorf("Want 0 keys, got %v", keys)
}
signer, _ := ssh.NewSignerFromKey(testPrivateKeys["rsa"])
if _, err := agent.Sign(signer.PublicKey(), []byte("hello")); err == nil {
t.Fatalf("Sign did not fail")
}
if err := agent.Remove(signer.PublicKey()); err == nil {
t.Fatalf("Remove did not fail")
}
if err := agent.RemoveAll(); err == nil {
t.Fatalf("RemoveAll did not fail")
}
if err := agent.Unlock(nil); err == nil {
t.Errorf("Unlock with wrong passphrase succeeded")
}
if err := agent.Unlock(passphrase); err != nil {
t.Errorf("Unlock: %v", err)
}
if err := agent.Remove(signer.PublicKey()); err != nil {
t.Fatalf("Remove: %v", err)
}
if keys, err := agent.List(); err != nil {
t.Errorf("List: %v", err)
} else if len(keys) != 1 {
t.Errorf("Want 1 keys, got %v", keys)
}
}

103
modules/crypto/ssh/agent/forward.go

@ -1,103 +0,0 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"errors"
"io"
"net"
"sync"
"github.com/gogits/gogs/modules/crypto/ssh"
)
// RequestAgentForwarding sets up agent forwarding for the session.
// ForwardToAgent or ForwardToRemote should be called to route
// the authentication requests.
func RequestAgentForwarding(session *ssh.Session) error {
ok, err := session.SendRequest("auth-agent-req@openssh.com", true, nil)
if err != nil {
return err
}
if !ok {
return errors.New("forwarding request denied")
}
return nil
}
// ForwardToAgent routes authentication requests to the given keyring.
func ForwardToAgent(client *ssh.Client, keyring Agent) error {
channels := client.HandleChannelOpen(channelType)
if channels == nil {
return errors.New("agent: already have handler for " + channelType)
}
go func() {
for ch := range channels {
channel, reqs, err := ch.Accept()
if err != nil {
continue
}
go ssh.DiscardRequests(reqs)
go func() {
ServeAgent(keyring, channel)
channel.Close()
}()
}
}()
return nil
}
const channelType = "auth-agent@openssh.com"
// ForwardToRemote routes authentication requests to the ssh-agent
// process serving on the given unix socket.
func ForwardToRemote(client *ssh.Client, addr string) error {
channels := client.HandleChannelOpen(channelType)
if channels == nil {
return errors.New("agent: already have handler for " + channelType)
}
conn, err := net.Dial("unix", addr)
if err != nil {
return err
}
conn.Close()
go func() {
for ch := range channels {
channel, reqs, err := ch.Accept()
if err != nil {
continue
}
go ssh.DiscardRequests(reqs)
go forwardUnixSocket(channel, addr)
}
}()
return nil
}
func forwardUnixSocket(channel ssh.Channel, addr string) {
conn, err := net.Dial("unix", addr)
if err != nil {
return
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
io.Copy(conn, channel)
conn.(*net.UnixConn).CloseWrite()
wg.Done()
}()
go func() {
io.Copy(channel, conn)
channel.CloseWrite()
wg.Done()
}()
wg.Wait()
conn.Close()
channel.Close()
}

184
modules/crypto/ssh/agent/keyring.go

@ -1,184 +0,0 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"bytes"
"crypto/rand"
"crypto/subtle"
"errors"
"fmt"
"sync"
"github.com/gogits/gogs/modules/crypto/ssh"
)
type privKey struct {
signer ssh.Signer
comment string
}
type keyring struct {
mu sync.Mutex
keys []privKey
locked bool
passphrase []byte
}
var errLocked = errors.New("agent: locked")
// NewKeyring returns an Agent that holds keys in memory. It is safe
// for concurrent use by multiple goroutines.
func NewKeyring() Agent {
return &keyring{}
}
// RemoveAll removes all identities.
func (r *keyring) RemoveAll() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
r.keys = nil
return nil
}
// Remove removes all identities with the given public key.
func (r *keyring) Remove(key ssh.PublicKey) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
want := key.Marshal()
found := false
for i := 0; i < len(r.keys); {
if bytes.Equal(r.keys[i].signer.PublicKey().Marshal(), want) {
found = true
r.keys[i] = r.keys[len(r.keys)-1]
r.keys = r.keys[len(r.keys)-1:]
continue
} else {
i++
}
}
if !found {
return errors.New("agent: key not found")
}
return nil
}
// Lock locks the agent. Sign and Remove will fail, and List will empty an empty list.
func (r *keyring) Lock(passphrase []byte) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
r.locked = true
r.passphrase = passphrase
return nil
}
// Unlock undoes the effect of Lock
func (r *keyring) Unlock(passphrase []byte) error {
r.mu.Lock()
defer r.mu.Unlock()
if !r.locked {
return errors.New("agent: not locked")
}
if len(passphrase) != len(r.passphrase) || 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
return fmt.Errorf("agent: incorrect passphrase")
}
r.locked = false
r.passphrase = nil
return nil
}
// List returns the identities known to the agent.
func (r *keyring) List() ([]*Key, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
// section 2.7: locked agents return empty.
return nil, nil
}
var ids []*Key
for _, k := range r.keys {
pub := k.signer.PublicKey()
ids = append(ids, &Key{
Format: pub.Type(),
Blob: pub.Marshal(),
Comment: k.comment})
}
return ids, nil
}
// Insert adds a private key to the keyring. If a certificate
// is given, that certificate is added as public key. Note that
// any constraints given are ignored.
func (r *keyring) Add(key AddedKey) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
if err != nil {
return err
}
if cert := key.Certificate; cert != nil {
signer, err = ssh.NewCertSigner(cert, signer)
if err != nil {
return err
}
}
r.keys = append(r.keys, privKey{signer, key.Comment})
return nil
}
// Sign returns a signature for the data.
func (r *keyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return nil, errLocked
}
wanted := key.Marshal()
for _, k := range r.keys {
if bytes.Equal(k.signer.PublicKey().Marshal(), wanted) {
return k.signer.Sign(rand.Reader, data)
}
}
return nil, errors.New("not found")
}
// Signers returns signers for all the known keys.
func (r *keyring) Signers() ([]ssh.Signer, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return nil, errLocked
}
s := make([]ssh.Signer, 0, len(r.keys))
for _, k := range r.keys {
s = append(s, k.signer)
}
return s, nil
}

209
modules/crypto/ssh/agent/server.go

@ -1,209 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"crypto/rsa"
"encoding/binary"
"fmt"
"io"
"log"
"math/big"
"github.com/gogits/gogs/modules/crypto/ssh"
)
// Server wraps an Agent and uses it to implement the agent side of
// the SSH-agent, wire protocol.
type server struct {
agent Agent
}
func (s *server) processRequestBytes(reqData []byte) []byte {
rep, err := s.processRequest(reqData)
if err != nil {
if err != errLocked {
// TODO(hanwen): provide better logging interface?
log.Printf("agent %d: %v", reqData[0], err)
}
return []byte{agentFailure}
}
if err == nil && rep == nil {
return []byte{agentSuccess}
}
return ssh.Marshal(rep)
}
func marshalKey(k *Key) []byte {
var record struct {
Blob []byte
Comment string
}
record.Blob = k.Marshal()
record.Comment = k.Comment
return ssh.Marshal(&record)
}
type agentV1IdentityMsg struct {
Numkeys uint32 `sshtype:"2"`
}
type agentRemoveIdentityMsg struct {
KeyBlob []byte `sshtype:"18"`
}
type agentLockMsg struct {
Passphrase []byte `sshtype:"22"`
}
type agentUnlockMsg struct {
Passphrase []byte `sshtype:"23"`
}
func (s *server) processRequest(data []byte) (interface{}, error) {
switch data[0] {
case agentRequestV1Identities:
return &agentV1IdentityMsg{0}, nil
case agentRemoveIdentity:
var req agentRemoveIdentityMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
var wk wireKey
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
return nil, err
}
return nil, s.agent.Remove(&Key{Format: wk.Format, Blob: req.KeyBlob})
case agentRemoveAllIdentities:
return nil, s.agent.RemoveAll()
case agentLock:
var req agentLockMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
return nil, s.agent.Lock(req.Passphrase)
case agentUnlock:
var req agentLockMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
return nil, s.agent.Unlock(req.Passphrase)
case agentSignRequest:
var req signRequestAgentMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
var wk wireKey
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
return nil, err
}
k := &Key{
Format: wk.Format,
Blob: req.KeyBlob,
}
sig, err := s.agent.Sign(k, req.Data) // TODO(hanwen): flags.
if err != nil {
return nil, err
}
return &signResponseAgentMsg{SigBlob: ssh.Marshal(sig)}, nil
case agentRequestIdentities:
keys, err := s.agent.List()
if err != nil {
return nil, err
}
rep := identitiesAnswerAgentMsg{
NumKeys: uint32(len(keys)),
}
for _, k := range keys {
rep.Keys = append(rep.Keys, marshalKey(k)...)
}
return rep, nil
case agentAddIdentity:
return nil, s.insertIdentity(data)
}
return nil, fmt.Errorf("unknown opcode %d", data[0])
}
func (s *server) insertIdentity(req []byte) error {
var record struct {
Type string `sshtype:"17"`
Rest []byte `ssh:"rest"`
}
if err := ssh.Unmarshal(req, &record); err != nil {
return err
}
switch record.Type {
case ssh.KeyAlgoRSA:
var k rsaKeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return err
}
priv := rsa.PrivateKey{
PublicKey: rsa.PublicKey{
E: int(k.E.Int64()),
N: k.N,
},
D: k.D,
Primes: []*big.Int{k.P, k.Q},
}
priv.Precompute()
return s.agent.Add(AddedKey{PrivateKey: &priv, Comment: k.Comments})
}
return fmt.Errorf("not implemented: %s", record.Type)
}
// ServeAgent serves the agent protocol on the given connection. It
// returns when an I/O error occurs.
func ServeAgent(agent Agent, c io.ReadWriter) error {
s := &server{agent}
var length [4]byte
for {
if _, err := io.ReadFull(c, length[:]); err != nil {
return err
}
l := binary.BigEndian.Uint32(length[:])
if l > maxAgentResponseBytes {
// We also cap requests.
return fmt.Errorf("agent: request too large: %d", l)
}
req := make([]byte, l)
if _, err := io.ReadFull(c, req); err != nil {
return err
}
repData := s.processRequestBytes(req)
if len(repData) > maxAgentResponseBytes {
return fmt.Errorf("agent: reply too large: %d bytes", len(repData))
}
binary.BigEndian.PutUint32(length[:], uint32(len(repData)))
if _, err := c.Write(length[:]); err != nil {
return err
}
if _, err := c.Write(repData); err != nil {
return err
}
}
}

77
modules/crypto/ssh/agent/server_test.go

@ -1,77 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"testing"
"github.com/gogits/gogs/modules/crypto/ssh"
)
func TestServer(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
client := NewClient(c1)
go ServeAgent(NewKeyring(), c2)
testAgentInterface(t, client, testPrivateKeys["rsa"], nil, 0)
}
func TestLockServer(t *testing.T) {
testLockAgent(NewKeyring(), t)
}
func TestSetupForwardAgent(t *testing.T) {
a, b, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer a.Close()
defer b.Close()
_, socket, cleanup := startAgent(t)
defer cleanup()
serverConf := ssh.ServerConfig{
NoClientAuth: true,
}
serverConf.AddHostKey(testSigners["rsa"])
incoming := make(chan *ssh.ServerConn, 1)
go func() {
conn, _, _, err := ssh.NewServerConn(a, &serverConf)
if err != nil {
t.Fatalf("Server: %v", err)
}
incoming <- conn
}()
conf := ssh.ClientConfig{}
conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf)
if err != nil {
t.Fatalf("NewClientConn: %v", err)
}
client := ssh.NewClient(conn, chans, reqs)
if err := ForwardToRemote(client, socket); err != nil {
t.Fatalf("SetupForwardAgent: %v", err)
}
server := <-incoming
ch, reqs, err := server.OpenChannel(channelType, nil)
if err != nil {
t.Fatalf("OpenChannel(%q): %v", channelType, err)
}
go ssh.DiscardRequests(reqs)
agentClient := NewClient(ch)
testAgentInterface(t, agentClient, testPrivateKeys["rsa"], nil, 0)
conn.Close()
}

64
modules/crypto/ssh/agent/testdata_test.go

@ -1,64 +0,0 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// IMPLEMENTOR NOTE: To avoid a package loop, this file is in three places:
// ssh/, ssh/agent, and ssh/test/. It should be kept in sync across all three
// instances.
package agent
import (
"crypto/rand"
"fmt"
"github.com/gogits/gogs/modules/crypto/ssh"
"github.com/gogits/gogs/modules/crypto/ssh/testdata"
)
var (
testPrivateKeys map[string]interface{}
testSigners map[string]ssh.Signer
testPublicKeys map[string]ssh.PublicKey
)
func init() {
var err error
n := len(testdata.PEMBytes)
testPrivateKeys = make(map[string]interface{}, n)
testSigners = make(map[string]ssh.Signer, n)
testPublicKeys = make(map[string]ssh.PublicKey, n)
for t, k := range testdata.PEMBytes {
testPrivateKeys[t], err = ssh.ParseRawPrivateKey(k)
if err != nil {
panic(fmt.Sprintf("Unable to parse test key %s: %v", t, err))
}
testSigners[t], err = ssh.NewSignerFromKey(testPrivateKeys[t])
if err != nil {
panic(fmt.Sprintf("Unable to create signer for test key %s: %v", t, err))
}
testPublicKeys[t] = testSigners[t].PublicKey()
}
// Create a cert and sign it for use in tests.
testCert := &ssh.Certificate{
Nonce: []byte{}, // To pass reflect.DeepEqual after marshal & parse, this must be non-nil
ValidPrincipals: []string{"gopher1", "gopher2"}, // increases test coverage
ValidAfter: 0, // unix epoch
ValidBefore: ssh.CertTimeInfinity, // The end of currently representable time.
Reserved: []byte{}, // To pass reflect.DeepEqual after marshal & parse, this must be non-nil
Key: testPublicKeys["ecdsa"],
SignatureKey: testPublicKeys["rsa"],
Permissions: ssh.Permissions{
CriticalOptions: map[string]string{},
Extensions: map[string]string{},
},
}
testCert.SignCert(rand.Reader, testSigners["rsa"])
testPrivateKeys["cert"] = testPrivateKeys["ecdsa"]
testSigners["cert"], err = ssh.NewCertSigner(testCert, testSigners["ecdsa"])
if err != nil {
panic(fmt.Sprintf("Unable to create certificate signer: %v", err))
}
}

122
modules/crypto/ssh/benchmark_test.go

@ -1,122 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"errors"
"io"
"net"
"testing"
)
type server struct {
*ServerConn
chans <-chan NewChannel
}
func newServer(c net.Conn, conf *ServerConfig) (*server, error) {
sconn, chans, reqs, err := NewServerConn(c, conf)
if err != nil {
return nil, err
}
go DiscardRequests(reqs)
return &server{sconn, chans}, nil
}
func (s *server) Accept() (NewChannel, error) {
n, ok := <-s.chans
if !ok {
return nil, io.EOF
}
return n, nil
}
func sshPipe() (Conn, *server, error) {
c1, c2, err := netPipe()
if err != nil {
return nil, nil, err
}
clientConf := ClientConfig{
User: "user",
}
serverConf := ServerConfig{
NoClientAuth: true,
}
serverConf.AddHostKey(testSigners["ecdsa"])
done := make(chan *server, 1)
go func() {
server, err := newServer(c2, &serverConf)
if err != nil {
done <- nil
}
done <- server
}()
client, _, reqs, err := NewClientConn(c1, "", &clientConf)
if err != nil {
return nil, nil, err
}
server := <-done
if server == nil {
return nil, nil, errors.New("server handshake failed.")
}
go DiscardRequests(reqs)
return client, server, nil
}
func BenchmarkEndToEnd(b *testing.B) {
b.StopTimer()
client, server, err := sshPipe()
if err != nil {
b.Fatalf("sshPipe: %v", err)
}
defer client.Close()
defer server.Close()
size := (1 << 20)
input := make([]byte, size)
output := make([]byte, size)
b.SetBytes(int64(size))
done := make(chan int, 1)
go func() {
newCh, err := server.Accept()
if err != nil {
b.Fatalf("Client: %v", err)
}
ch, incoming, err := newCh.Accept()
go DiscardRequests(incoming)
for i := 0; i < b.N; i++ {
if _, err := io.ReadFull(ch, output); err != nil {
b.Fatalf("ReadFull: %v", err)
}
}
ch.Close()
done <- 1
}()
ch, in, err := client.OpenChannel("speed", nil)
if err != nil {
b.Fatalf("OpenChannel: %v", err)
}
go DiscardRequests(in)
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
if _, err := ch.Write(input); err != nil {
b.Fatalf("WriteFull: %v", err)
}
}
ch.Close()
b.StopTimer()
<-done
}

98
modules/crypto/ssh/buffer.go

@ -1,98 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"io"
"sync"
)
// buffer provides a linked list buffer for data exchange
// between producer and consumer. Theoretically the buffer is
// of unlimited capacity as it does no allocation of its own.
type buffer struct {
// protects concurrent access to head, tail and closed
*sync.Cond
head *element // the buffer that will be read first
tail *element // the buffer that will be read last
closed bool
}
// An element represents a single link in a linked list.
type element struct {
buf []byte
next *element
}
// newBuffer returns an empty buffer that is not closed.
func newBuffer() *buffer {
e := new(element)
b := &buffer{
Cond: newCond(),
head: e,
tail: e,
}
return b
}
// write makes buf available for Read to receive.
// buf must not be modified after the call to write.
func (b *buffer) write(buf []byte) {
b.Cond.L.Lock()
e := &element{buf: buf}
b.tail.next = e
b.tail = e
b.Cond.Signal()
b.Cond.L.Unlock()
}
// eof closes the buffer. Reads from the buffer once all
// the data has been consumed will receive os.EOF.
func (b *buffer) eof() error {
b.Cond.L.Lock()
b.closed = true
b.Cond.Signal()
b.Cond.L.Unlock()
return nil
}
// Read reads data from the internal buffer in buf. Reads will block
// if no data is available, or until the buffer is closed.
func (b *buffer) Read(buf []byte) (n int, err error) {
b.Cond.L.Lock()
defer b.Cond.L.Unlock()
for len(buf) > 0 {
// if there is data in b.head, copy it
if len(b.head.buf) > 0 {
r := copy(buf, b.head.buf)
buf, b.head.buf = buf[r:], b.head.buf[r:]
n += r
continue
}
// if there is a next buffer, make it the head
if len(b.head.buf) == 0 && b.head != b.tail {
b.head = b.head.next
continue
}
// if at least one byte has been copied, return
if n > 0 {
break
}
// if nothing was read, and there is nothing outstanding
// check to see if the buffer is closed.
if b.closed {
err = io.EOF
break
}
// out of buffers, wait for producer
b.Cond.Wait()
}
return
}

87
modules/crypto/ssh/buffer_test.go

@ -1,87 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"io"
"testing"
)
var alphabet = []byte("abcdefghijklmnopqrstuvwxyz")
func TestBufferReadwrite(t *testing.T) {
b := newBuffer()
b.write(alphabet[:10])
r, _ := b.Read(make([]byte, 10))
if r != 10 {
t.Fatalf("Expected written == read == 10, written: 10, read %d", r)
}
b = newBuffer()
b.write(alphabet[:5])
r, _ = b.Read(make([]byte, 10))
if r != 5 {
t.Fatalf("Expected written == read == 5, written: 5, read %d", r)
}
b = newBuffer()
b.write(alphabet[:10])
r, _ = b.Read(make([]byte, 5))
if r != 5 {
t.Fatalf("Expected written == 10, read == 5, written: 10, read %d", r)
}
b = newBuffer()
b.write(alphabet[:5])
b.write(alphabet[5:15])
r, _ = b.Read(make([]byte, 10))
r2, _ := b.Read(make([]byte, 10))
if r != 10 || r2 != 5 || 15 != r+r2 {
t.Fatal("Expected written == read == 15")
}
}
func TestBufferClose(t *testing.T) {
b := newBuffer()
b.write(alphabet[:10])
b.eof()
_, err := b.Read(make([]byte, 5))
if err != nil {
t.Fatal("expected read of 5 to not return EOF")
}
b = newBuffer()
b.write(alphabet[:10])
b.eof()
r, err := b.Read(make([]byte, 5))
r2, err2 := b.Read(make([]byte, 10))
if r != 5 || r2 != 5 || err != nil || err2 != nil {
t.Fatal("expected reads of 5 and 5")
}
b = newBuffer()
b.write(alphabet[:10])
b.eof()
r, err = b.Read(make([]byte, 5))
r2, err2 = b.Read(make([]byte, 10))
r3, err3 := b.Read(make([]byte, 10))
if r != 5 || r2 != 5 || r3 != 0 || err != nil || err2 != nil || err3 != io.EOF {
t.Fatal("expected reads of 5 and 5 and 0, with EOF")
}
b = newBuffer()
b.write(make([]byte, 5))
b.write(make([]byte, 10))
b.eof()
r, err = b.Read(make([]byte, 9))
r2, err2 = b.Read(make([]byte, 3))
r3, err3 = b.Read(make([]byte, 3))
r4, err4 := b.Read(make([]byte, 10))
if err != nil || err2 != nil || err3 != nil || err4 != io.EOF {
t.Fatalf("Expected EOF on forth read only, err=%v, err2=%v, err3=%v, err4=%v", err, err2, err3, err4)
}
if r != 9 || r2 != 3 || r3 != 3 || r4 != 0 {
t.Fatal("Expected written == read == 15", r, r2, r3, r4)
}
}

501
modules/crypto/ssh/certs.go

@ -1,501 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"sort"
"time"
)
// These constants from [PROTOCOL.certkeys] represent the algorithm names
// for certificate types supported by this package.
const (
CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
CertAlgoDSAv01 = "ssh-dss-cert-v01@openssh.com"
CertAlgoECDSA256v01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
CertAlgoECDSA384v01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
CertAlgoECDSA521v01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
)
// Certificate types distinguish between host and user
// certificates. The values can be set in the CertType field of
// Certificate.
const (
UserCert = 1
HostCert = 2
)
// Signature represents a cryptographic signature.
type Signature struct {
Format string
Blob []byte
}
// CertTimeInfinity can be used for OpenSSHCertV01.ValidBefore to indicate that
// a certificate does not expire.
const CertTimeInfinity = 1<<64 - 1
// An Certificate represents an OpenSSH certificate as defined in
// [PROTOCOL.certkeys]?rev=1.8.
type Certificate struct {
Nonce []byte
Key PublicKey
Serial uint64
CertType uint32
KeyId string
ValidPrincipals []string
ValidAfter uint64
ValidBefore uint64
Permissions
Reserved []byte
SignatureKey PublicKey
Signature *Signature
}
// genericCertData holds the key-independent part of the certificate data.
// Overall, certificates contain an nonce, public key fields and
// key-independent fields.
type genericCertData struct {
Serial uint64
CertType uint32
KeyId string
ValidPrincipals []byte
ValidAfter uint64
ValidBefore uint64
CriticalOptions []byte
Extensions []byte
Reserved []byte
SignatureKey []byte
Signature []byte
}
func marshalStringList(namelist []string) []byte {
var to []byte
for _, name := range namelist {
s := struct{ N string }{name}
to = append(to, Marshal(&s)...)
}
return to
}
type optionsTuple struct {
Key string
Value []byte
}
type optionsTupleValue struct {
Value string
}
// serialize a map of critical options or extensions
// issue #10569 - per [PROTOCOL.certkeys] and SSH implementation,
// we need two length prefixes for a non-empty string value
func marshalTuples(tups map[string]string) []byte {
keys := make([]string, 0, len(tups))
for key := range tups {
keys = append(keys, key)
}
sort.Strings(keys)
var ret []byte
for _, key := range keys {
s := optionsTuple{Key: key}
if value := tups[key]; len(value) > 0 {
s.Value = Marshal(&optionsTupleValue{value})
}
ret = append(ret, Marshal(&s)...)
}
return ret
}
// issue #10569 - per [PROTOCOL.certkeys] and SSH implementation,
// we need two length prefixes for a non-empty option value
func parseTuples(in []byte) (map[string]string, error) {
tups := map[string]string{}
var lastKey string
var haveLastKey bool
for len(in) > 0 {
var key, val, extra []byte
var ok bool
if key, in, ok = parseString(in); !ok {
return nil, errShortRead
}
keyStr := string(key)
// according to [PROTOCOL.certkeys], the names must be in
// lexical order.
if haveLastKey && keyStr <= lastKey {
return nil, fmt.Errorf("ssh: certificate options are not in lexical order")
}
lastKey, haveLastKey = keyStr, true
// the next field is a data field, which if non-empty has a string embedded
if val, in, ok = parseString(in); !ok {
return nil, errShortRead
}
if len(val) > 0 {
val, extra, ok = parseString(val)
if !ok {
return nil, errShortRead
}
if len(extra) > 0 {
return nil, fmt.Errorf("ssh: unexpected trailing data after certificate option value")
}
tups[keyStr] = string(val)
} else {
tups[keyStr] = ""
}
}
return tups, nil
}
func parseCert(in []byte, privAlgo string) (*Certificate, error) {
nonce, rest, ok := parseString(in)
if !ok {
return nil, errShortRead
}
key, rest, err := parsePubKey(rest, privAlgo)
if err != nil {
return nil, err
}
var g genericCertData
if err := Unmarshal(rest, &g); err != nil {
return nil, err
}
c := &Certificate{
Nonce: nonce,
Key: key,
Serial: g.Serial,
CertType: g.CertType,
KeyId: g.KeyId,
ValidAfter: g.ValidAfter,
ValidBefore: g.ValidBefore,
}
for principals := g.ValidPrincipals; len(principals) > 0; {
principal, rest, ok := parseString(principals)
if !ok {
return nil, errShortRead
}
c.ValidPrincipals = append(c.ValidPrincipals, string(principal))
principals = rest
}
c.CriticalOptions, err = parseTuples(g.CriticalOptions)
if err != nil {
return nil, err
}
c.Extensions, err = parseTuples(g.Extensions)
if err != nil {
return nil, err
}
c.Reserved = g.Reserved
k, err := ParsePublicKey(g.SignatureKey)
if err != nil {
return nil, err
}
c.SignatureKey = k
c.Signature, rest, ok = parseSignatureBody(g.Signature)
if !ok || len(rest) > 0 {
return nil, errors.New("ssh: signature parse error")
}
return c, nil
}
type openSSHCertSigner struct {
pub *Certificate
signer Signer
}
// NewCertSigner returns a Signer that signs with the given Certificate, whose
// private key is held by signer. It returns an error if the public key in cert
// doesn't match the key used by signer.
func NewCertSigner(cert *Certificate, signer Signer) (Signer, error) {
if bytes.Compare(cert.Key.Marshal(), signer.PublicKey().Marshal()) != 0 {
return nil, errors.New("ssh: signer and cert have different public key")
}
return &openSSHCertSigner{cert, signer}, nil
}
func (s *openSSHCertSigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
return s.signer.Sign(rand, data)
}
func (s *openSSHCertSigner) PublicKey() PublicKey {
return s.pub
}
const sourceAddressCriticalOption = "source-address"
// CertChecker does the work of verifying a certificate. Its methods
// can be plugged into ClientConfig.HostKeyCallback and
// ServerConfig.PublicKeyCallback. For the CertChecker to work,
// minimally, the IsAuthority callback should be set.
type CertChecker struct {
// SupportedCriticalOptions lists the CriticalOptions that the
// server application layer understands. These are only used
// for user certificates.
SupportedCriticalOptions []string
// IsAuthority should return true if the key is recognized as
// an authority. This allows for certificates to be signed by other
// certificates.
IsAuthority func(auth PublicKey) bool
// Clock is used for verifying time stamps. If nil, time.Now
// is used.
Clock func() time.Time
// UserKeyFallback is called when CertChecker.Authenticate encounters a
// public key that is not a certificate. It must implement validation
// of user keys or else, if nil, all such keys are rejected.
UserKeyFallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)
// HostKeyFallback is called when CertChecker.CheckHostKey encounters a
// public key that is not a certificate. It must implement host key
// validation or else, if nil, all such keys are rejected.
HostKeyFallback func(addr string, remote net.Addr, key PublicKey) error
// IsRevoked is called for each certificate so that revocation checking
// can be implemented. It should return true if the given certificate
// is revoked and false otherwise. If nil, no certificates are
// considered to have been revoked.
IsRevoked func(cert *Certificate) bool
}
// CheckHostKey checks a host key certificate. This method can be
// plugged into ClientConfig.HostKeyCallback.
func (c *CertChecker) CheckHostKey(addr string, remote net.Addr, key PublicKey) error {
cert, ok := key.(*Certificate)
if !ok {
if c.HostKeyFallback != nil {
return c.HostKeyFallback(addr, remote, key)
}
return errors.New("ssh: non-certificate host key")
}
if cert.CertType != HostCert {
return fmt.Errorf("ssh: certificate presented as a host key has type %d", cert.CertType)
}
return c.CheckCert(addr, cert)
}
// Authenticate checks a user certificate. Authenticate can be used as
// a value for ServerConfig.PublicKeyCallback.
func (c *CertChecker) Authenticate(conn ConnMetadata, pubKey PublicKey) (*Permissions, error) {
cert, ok := pubKey.(*Certificate)
if !ok {
if c.UserKeyFallback != nil {
return c.UserKeyFallback(conn, pubKey)
}
return nil, errors.New("ssh: normal key pairs not accepted")
}
if cert.CertType != UserCert {
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
}
if err := c.CheckCert(conn.User(), cert); err != nil {
return nil, err
}
return &cert.Permissions, nil
}
// CheckCert checks CriticalOptions, ValidPrincipals, revocation, timestamp and
// the signature of the certificate.
func (c *CertChecker) CheckCert(principal string, cert *Certificate) error {
if c.IsRevoked != nil && c.IsRevoked(cert) {
return fmt.Errorf("ssh: certicate serial %d revoked", cert.Serial)
}
for opt, _ := range cert.CriticalOptions {
// sourceAddressCriticalOption will be enforced by
// serverAuthenticate
if opt == sourceAddressCriticalOption {
continue
}
found := false
for _, supp := range c.SupportedCriticalOptions {
if supp == opt {
found = true
break
}
}
if !found {
return fmt.Errorf("ssh: unsupported critical option %q in certificate", opt)
}
}
if len(cert.ValidPrincipals) > 0 {
// By default, certs are valid for all users/hosts.
found := false
for _, p := range cert.ValidPrincipals {
if p == principal {
found = true
break
}
}
if !found {
return fmt.Errorf("ssh: principal %q not in the set of valid principals for given certificate: %q", principal, cert.ValidPrincipals)
}
}
if !c.IsAuthority(cert.SignatureKey) {
return fmt.Errorf("ssh: certificate signed by unrecognized authority")
}
clock := c.Clock
if clock == nil {
clock = time.Now
}
unixNow := clock().Unix()
if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
return fmt.Errorf("ssh: cert is not yet valid")
}
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(CertTimeInfinity) && (unixNow >= before || before < 0) {
return fmt.Errorf("ssh: cert has expired")
}
if err := cert.SignatureKey.Verify(cert.bytesForSigning(), cert.Signature); err != nil {
return fmt.Errorf("ssh: certificate signature does not verify")
}
return nil
}
// SignCert sets c.SignatureKey to the authority's public key and stores a
// Signature, by authority, in the certificate.
func (c *Certificate) SignCert(rand io.Reader, authority Signer) error {
c.Nonce = make([]byte, 32)
if _, err := io.ReadFull(rand, c.Nonce); err != nil {
return err
}
c.SignatureKey = authority.PublicKey()
sig, err := authority.Sign(rand, c.bytesForSigning())
if err != nil {
return err
}
c.Signature = sig
return nil
}
var certAlgoNames = map[string]string{
KeyAlgoRSA: CertAlgoRSAv01,
KeyAlgoDSA: CertAlgoDSAv01,
KeyAlgoECDSA256: CertAlgoECDSA256v01,
KeyAlgoECDSA384: CertAlgoECDSA384v01,
KeyAlgoECDSA521: CertAlgoECDSA521v01,
}
// certToPrivAlgo returns the underlying algorithm for a certificate algorithm.
// Panics if a non-certificate algorithm is passed.
func certToPrivAlgo(algo string) string {
for privAlgo, pubAlgo := range certAlgoNames {
if pubAlgo == algo {
return privAlgo
}
}
panic("unknown cert algorithm")
}
func (cert *Certificate) bytesForSigning() []byte {
c2 := *cert
c2.Signature = nil
out := c2.Marshal()
// Drop trailing signature length.
return out[:len(out)-4]
}
// Marshal serializes c into OpenSSH's wire format. It is part of the
// PublicKey interface.
func (c *Certificate) Marshal() []byte {
generic := genericCertData{
Serial: c.Serial,
CertType: c.CertType,
KeyId: c.KeyId,
ValidPrincipals: marshalStringList(c.ValidPrincipals),
ValidAfter: uint64(c.ValidAfter),
ValidBefore: uint64(c.ValidBefore),
CriticalOptions: marshalTuples(c.CriticalOptions),
Extensions: marshalTuples(c.Extensions),
Reserved: c.Reserved,
SignatureKey: c.SignatureKey.Marshal(),
}
if c.Signature != nil {
generic.Signature = Marshal(c.Signature)
}
genericBytes := Marshal(&generic)
keyBytes := c.Key.Marshal()
_, keyBytes, _ = parseString(keyBytes)
prefix := Marshal(&struct {
Name string
Nonce []byte
Key []byte `ssh:"rest"`
}{c.Type(), c.Nonce, keyBytes})
result := make([]byte, 0, len(prefix)+len(genericBytes))
result = append(result, prefix...)
result = append(result, genericBytes...)
return result
}
// Type returns the key name. It is part of the PublicKey interface.
func (c *Certificate) Type() string {
algo, ok := certAlgoNames[c.Key.Type()]
if !ok {
panic("unknown cert key type")
}
return algo
}
// Verify verifies a signature against the certificate's public
// key. It is part of the PublicKey interface.
func (c *Certificate) Verify(data []byte, sig *Signature) error {
return c.Key.Verify(data, sig)
}
func parseSignatureBody(in []byte) (out *Signature, rest []byte, ok bool) {
format, in, ok := parseString(in)
if !ok {
return
}
out = &Signature{
Format: string(format),
}
if out.Blob, in, ok = parseString(in); !ok {
return
}
return out, in, ok
}
func parseSignature(in []byte) (out *Signature, rest []byte, ok bool) {
sigBytes, rest, ok := parseString(in)
if !ok {
return
}
out, trailing, ok := parseSignatureBody(sigBytes)
if !ok || len(trailing) > 0 {
return nil, nil, false
}
return
}

216
modules/crypto/ssh/certs_test.go

@ -1,216 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto/rand"
"reflect"
"testing"
"time"
)
// Cert generated by ssh-keygen 6.0p1 Debian-4.
// % ssh-keygen -s ca-key -I test user-key
const exampleSSHCert = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgb1srW/W3ZDjYAO45xLYAwzHBDLsJ4Ux6ICFIkTjb1LEAAAADAQABAAAAYQCkoR51poH0wE8w72cqSB8Sszx+vAhzcMdCO0wqHTj7UNENHWEXGrU0E0UQekD7U+yhkhtoyjbPOVIP7hNa6aRk/ezdh/iUnCIt4Jt1v3Z1h1P+hA4QuYFMHNB+rmjPwAcAAAAAAAAAAAAAAAEAAAAEdGVzdAAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAHcAAAAHc3NoLXJzYQAAAAMBAAEAAABhANFS2kaktpSGc+CcmEKPyw9mJC4nZKxHKTgLVZeaGbFZOvJTNzBspQHdy7Q1uKSfktxpgjZnksiu/tFF9ngyY2KFoc+U88ya95IZUycBGCUbBQ8+bhDtw/icdDGQD5WnUwAAAG8AAAAHc3NoLXJzYQAAAGC8Y9Z2LQKhIhxf52773XaWrXdxP0t3GBVo4A10vUWiYoAGepr6rQIoGGXFxT4B9Gp+nEBJjOwKDXPrAevow0T9ca8gZN+0ykbhSrXLE5Ao48rqr3zP4O1/9P7e6gp0gw8=`
func TestParseCert(t *testing.T) {
authKeyBytes := []byte(exampleSSHCert)
key, _, _, rest, err := ParseAuthorizedKey(authKeyBytes)
if err != nil {
t.Fatalf("ParseAuthorizedKey: %v", err)
}
if len(rest) > 0 {
t.Errorf("rest: got %q, want empty", rest)
}
if _, ok := key.(*Certificate); !ok {
t.Fatalf("got %v (%T), want *Certificate", key, key)
}
marshaled := MarshalAuthorizedKey(key)
// Before comparison, remove the trailing newline that
// MarshalAuthorizedKey adds.
marshaled = marshaled[:len(marshaled)-1]
if !bytes.Equal(authKeyBytes, marshaled) {
t.Errorf("marshaled certificate does not match original: got %q, want %q", marshaled, authKeyBytes)
}
}
// Cert generated by ssh-keygen OpenSSH_6.8p1 OS X 10.10.3
// % ssh-keygen -s ca -I testcert -O source-address=192.168.1.0/24 -O force-command=/bin/sleep user.pub
// user.pub key: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDACh1rt2DXfV3hk6fszSQcQ/rueMId0kVD9U7nl8cfEnFxqOCrNT92g4laQIGl2mn8lsGZfTLg8ksHq3gkvgO3oo/0wHy4v32JeBOHTsN5AL4gfHNEhWeWb50ev47hnTsRIt9P4dxogeUo/hTu7j9+s9lLpEQXCvq6xocXQt0j8MV9qZBBXFLXVT3cWIkSqOdwt/5ZBg+1GSrc7WfCXVWgTk4a20uPMuJPxU4RQwZW6X3+O8Pqo8C3cW0OzZRFP6gUYUKUsTI5WntlS+LAxgw1mZNsozFGdbiOPRnEryE3SRldh9vjDR3tin1fGpA5P7+CEB/bqaXtG3V+F2OkqaMN
// Critical Options:
// force-command /bin/sleep
// source-address 192.168.1.0/24
// Extensions:
// permit-X11-forwarding
// permit-agent-forwarding
// permit-port-forwarding
// permit-pty
// permit-user-rc
const exampleSSHCertWithOptions = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgDyysCJY0XrO1n03EeRRoITnTPdjENFmWDs9X58PP3VUAAAADAQABAAABAQDACh1rt2DXfV3hk6fszSQcQ/rueMId0kVD9U7nl8cfEnFxqOCrNT92g4laQIGl2mn8lsGZfTLg8ksHq3gkvgO3oo/0wHy4v32JeBOHTsN5AL4gfHNEhWeWb50ev47hnTsRIt9P4dxogeUo/hTu7j9+s9lLpEQXCvq6xocXQt0j8MV9qZBBXFLXVT3cWIkSqOdwt/5ZBg+1GSrc7WfCXVWgTk4a20uPMuJPxU4RQwZW6X3+O8Pqo8C3cW0OzZRFP6gUYUKUsTI5WntlS+LAxgw1mZNsozFGdbiOPRnEryE3SRldh9vjDR3tin1fGpA5P7+CEB/bqaXtG3V+F2OkqaMNAAAAAAAAAAAAAAABAAAACHRlc3RjZXJ0AAAAAAAAAAAAAAAA//////////8AAABLAAAADWZvcmNlLWNvbW1hbmQAAAAOAAAACi9iaW4vc2xlZXAAAAAOc291cmNlLWFkZHJlc3MAAAASAAAADjE5Mi4xNjguMS4wLzI0AAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABFwAAAAdzc2gtcnNhAAAAAwEAAQAAAQEAwU+c5ui5A8+J/CFpjW8wCa52bEODA808WWQDCSuTG/eMXNf59v9Y8Pk0F1E9dGCosSNyVcB/hacUrc6He+i97+HJCyKavBsE6GDxrjRyxYqAlfcOXi/IVmaUGiO8OQ39d4GHrjToInKvExSUeleQyH4Y4/e27T/pILAqPFL3fyrvMLT5qU9QyIt6zIpa7GBP5+urouNavMprV3zsfIqNBbWypinOQAw823a5wN+zwXnhZrgQiHZ/USG09Y6k98y1dTVz8YHlQVR4D3lpTAsKDKJ5hCH9WU4fdf+lU8OyNGaJ/vz0XNqxcToe1l4numLTnaoSuH89pHryjqurB7lJKwAAAQ8AAAAHc3NoLXJzYQAAAQCaHvUIoPL1zWUHIXLvu96/HU1s/i4CAW2IIEuGgxCUCiFj6vyTyYtgxQxcmbfZf6eaITlS6XJZa7Qq4iaFZh75C1DXTX8labXhRSD4E2t//AIP9MC1rtQC5xo6FmbQ+BoKcDskr+mNACcbRSxs3IL3bwCfWDnIw2WbVox9ZdcthJKk4UoCW4ix4QwdHw7zlddlz++fGEEVhmTbll1SUkycGApPFBsAYRTMupUJcYPIeReBI/m8XfkoMk99bV8ZJQTAd7OekHY2/48Ff53jLmyDjP7kNw1F8OaPtkFs6dGJXta4krmaekPy87j+35In5hFj7yoOqvSbmYUkeX70/GGQ`
func TestParseCertWithOptions(t *testing.T) {
opts := map[string]string{
"source-address": "192.168.1.0/24",
"force-command": "/bin/sleep",
}
exts := map[string]string{
"permit-X11-forwarding": "",
"permit-agent-forwarding": "",
"permit-port-forwarding": "",
"permit-pty": "",
"permit-user-rc": "",
}
authKeyBytes := []byte(exampleSSHCertWithOptions)
key, _, _, rest, err := ParseAuthorizedKey(authKeyBytes)
if err != nil {
t.Fatalf("ParseAuthorizedKey: %v", err)
}
if len(rest) > 0 {
t.Errorf("rest: got %q, want empty", rest)
}
cert, ok := key.(*Certificate)
if !ok {
t.Fatalf("got %v (%T), want *Certificate", key, key)
}
if !reflect.DeepEqual(cert.CriticalOptions, opts) {
t.Errorf("unexpected critical options - got %v, want %v", cert.CriticalOptions, opts)
}
if !reflect.DeepEqual(cert.Extensions, exts) {
t.Errorf("unexpected Extensions - got %v, want %v", cert.Extensions, exts)
}
marshaled := MarshalAuthorizedKey(key)
// Before comparison, remove the trailing newline that
// MarshalAuthorizedKey adds.
marshaled = marshaled[:len(marshaled)-1]
if !bytes.Equal(authKeyBytes, marshaled) {
t.Errorf("marshaled certificate does not match original: got %q, want %q", marshaled, authKeyBytes)
}
}
func TestValidateCert(t *testing.T) {
key, _, _, _, err := ParseAuthorizedKey([]byte(exampleSSHCert))
if err != nil {
t.Fatalf("ParseAuthorizedKey: %v", err)
}
validCert, ok := key.(*Certificate)
if !ok {
t.Fatalf("got %v (%T), want *Certificate", key, key)
}
checker := CertChecker{}
checker.IsAuthority = func(k PublicKey) bool {
return bytes.Equal(k.Marshal(), validCert.SignatureKey.Marshal())
}
if err := checker.CheckCert("user", validCert); err != nil {
t.Errorf("Unable to validate certificate: %v", err)
}
invalidCert := &Certificate{
Key: testPublicKeys["rsa"],
SignatureKey: testPublicKeys["ecdsa"],
ValidBefore: CertTimeInfinity,
Signature: &Signature{},
}
if err := checker.CheckCert("user", invalidCert); err == nil {
t.Error("Invalid cert signature passed validation")
}
}
func TestValidateCertTime(t *testing.T) {
cert := Certificate{
ValidPrincipals: []string{"user"},
Key: testPublicKeys["rsa"],
ValidAfter: 50,
ValidBefore: 100,
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
for ts, ok := range map[int64]bool{
25: false,
50: true,
99: true,
100: false,
125: false,
} {
checker := CertChecker{
Clock: func() time.Time { return time.Unix(ts, 0) },
}
checker.IsAuthority = func(k PublicKey) bool {
return bytes.Equal(k.Marshal(),
testPublicKeys["ecdsa"].Marshal())
}
if v := checker.CheckCert("user", &cert); (v == nil) != ok {
t.Errorf("Authenticate(%d): %v", ts, v)
}
}
}
// TODO(hanwen): tests for
//
// host keys:
// * fallbacks
func TestHostKeyCert(t *testing.T) {
cert := &Certificate{
ValidPrincipals: []string{"hostname", "hostname.domain"},
Key: testPublicKeys["rsa"],
ValidBefore: CertTimeInfinity,
CertType: HostCert,
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
checker := &CertChecker{
IsAuthority: func(p PublicKey) bool {
return bytes.Equal(testPublicKeys["ecdsa"].Marshal(), p.Marshal())
},
}
certSigner, err := NewCertSigner(cert, testSigners["rsa"])
if err != nil {
t.Errorf("NewCertSigner: %v", err)
}
for _, name := range []string{"hostname", "otherhost"} {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
errc := make(chan error)
go func() {
conf := ServerConfig{
NoClientAuth: true,
}
conf.AddHostKey(certSigner)
_, _, _, err := NewServerConn(c1, &conf)
errc <- err
}()
config := &ClientConfig{
User: "user",
HostKeyCallback: checker.CheckHostKey,
}
_, _, _, err = NewClientConn(c2, name, config)
succeed := name == "hostname"
if (err == nil) != succeed {
t.Fatalf("NewClientConn(%q): %v", name, err)
}
err = <-errc
if (err == nil) != succeed {
t.Fatalf("NewServerConn(%q): %v", name, err)
}
}
}

631
modules/crypto/ssh/channel.go

@ -1,631 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"sync"
)
const (
minPacketLength = 9
// channelMaxPacket contains the maximum number of bytes that will be
// sent in a single packet. As per RFC 4253, section 6.1, 32k is also
// the minimum.
channelMaxPacket = 1 << 15
// We follow OpenSSH here.
channelWindowSize = 64 * channelMaxPacket
)
// NewChannel represents an incoming request to a channel. It must either be
// accepted for use by calling Accept, or rejected by calling Reject.
type NewChannel interface {
// Accept accepts the channel creation request. It returns the Channel
// and a Go channel containing SSH requests. The Go channel must be
// serviced otherwise the Channel will hang.
Accept() (Channel, <-chan *Request, error)
// Reject rejects the channel creation request. After calling
// this, no other methods on the Channel may be called.
Reject(reason RejectionReason, message string) error
// ChannelType returns the type of the channel, as supplied by the
// client.
ChannelType() string
// ExtraData returns the arbitrary payload for this channel, as supplied
// by the client. This data is specific to the channel type.
ExtraData() []byte
}
// A Channel is an ordered, reliable, flow-controlled, duplex stream
// that is multiplexed over an SSH connection.
type Channel interface {
// Read reads up to len(data) bytes from the channel.
Read(data []byte) (int, error)
// Write writes len(data) bytes to the channel.
Write(data []byte) (int, error)
// Close signals end of channel use. No data may be sent after this
// call.
Close() error
// CloseWrite signals the end of sending in-band
// data. Requests may still be sent, and the other side may
// still send data
CloseWrite() error
// SendRequest sends a channel request. If wantReply is true,
// it will wait for a reply and return the result as a
// boolean, otherwise the return value will be false. Channel
// requests are out-of-band messages so they may be sent even
// if the data stream is closed or blocked by flow control.
SendRequest(name string, wantReply bool, payload []byte) (bool, error)
// Stderr returns an io.ReadWriter that writes to this channel
// with the extended data type set to stderr. Stderr may
// safely be read and written from a different goroutine than
// Read and Write respectively.
Stderr() io.ReadWriter
}
// Request is a request sent outside of the normal stream of
// data. Requests can either be specific to an SSH channel, or they
// can be global.
type Request struct {
Type string
WantReply bool
Payload []byte
ch *channel
mux *mux
}
// Reply sends a response to a request. It must be called for all requests
// where WantReply is true and is a no-op otherwise. The payload argument is
// ignored for replies to channel-specific requests.
func (r *Request) Reply(ok bool, payload []byte) error {
if !r.WantReply {
return nil
}
if r.ch == nil {
return r.mux.ackRequest(ok, payload)
}
return r.ch.ackRequest(ok)
}
// RejectionReason is an enumeration used when rejecting channel creation
// requests. See RFC 4254, section 5.1.
type RejectionReason uint32
const (
Prohibited RejectionReason = iota + 1
ConnectionFailed
UnknownChannelType
ResourceShortage
)
// String converts the rejection reason to human readable form.
func (r RejectionReason) String() string {
switch r {
case Prohibited:
return "administratively prohibited"
case ConnectionFailed:
return "connect failed"
case UnknownChannelType:
return "unknown channel type"
case ResourceShortage:
return "resource shortage"
}
return fmt.Sprintf("unknown reason %d", int(r))
}
func min(a uint32, b int) uint32 {
if a < uint32(b) {
return a
}
return uint32(b)
}
type channelDirection uint8
const (
channelInbound channelDirection = iota
channelOutbound
)
// channel is an implementation of the Channel interface that works
// with the mux class.
type channel struct {
// R/O after creation
chanType string
extraData []byte
localId, remoteId uint32
// maxIncomingPayload and maxRemotePayload are the maximum
// payload sizes of normal and extended data packets for
// receiving and sending, respectively. The wire packet will
// be 9 or 13 bytes larger (excluding encryption overhead).
maxIncomingPayload uint32
maxRemotePayload uint32
mux *mux
// decided is set to true if an accept or reject message has been sent
// (for outbound channels) or received (for inbound channels).
decided bool
// direction contains either channelOutbound, for channels created
// locally, or channelInbound, for channels created by the peer.
direction channelDirection
// Pending internal channel messages.
msg chan interface{}
// Since requests have no ID, there can be only one request
// with WantReply=true outstanding. This lock is held by a
// goroutine that has such an outgoing request pending.
sentRequestMu sync.Mutex
incomingRequests chan *Request
sentEOF bool
// thread-safe data
remoteWin window
pending *buffer
extPending *buffer
// windowMu protects myWindow, the flow-control window.
windowMu sync.Mutex
myWindow uint32
// writeMu serializes calls to mux.conn.writePacket() and
// protects sentClose and packetPool. This mutex must be
// different from windowMu, as writePacket can block if there
// is a key exchange pending.
writeMu sync.Mutex
sentClose bool
// packetPool has a buffer for each extended channel ID to
// save allocations during writes.
packetPool map[uint32][]byte
}
// writePacket sends a packet. If the packet is a channel close, it updates
// sentClose. This method takes the lock c.writeMu.
func (c *channel) writePacket(packet []byte) error {
c.writeMu.Lock()
if c.sentClose {
c.writeMu.Unlock()
return io.EOF
}
c.sentClose = (packet[0] == msgChannelClose)
err := c.mux.conn.writePacket(packet)
c.writeMu.Unlock()
return err
}
func (c *channel) sendMessage(msg interface{}) error {
if debugMux {
log.Printf("send %d: %#v", c.mux.chanList.offset, msg)
}
p := Marshal(msg)
binary.BigEndian.PutUint32(p[1:], c.remoteId)
return c.writePacket(p)
}
// WriteExtended writes data to a specific extended stream. These streams are
// used, for example, for stderr.
func (c *channel) WriteExtended(data []byte, extendedCode uint32) (n int, err error) {
if c.sentEOF {
return 0, io.EOF
}
// 1 byte message type, 4 bytes remoteId, 4 bytes data length
opCode := byte(msgChannelData)
headerLength := uint32(9)
if extendedCode > 0 {
headerLength += 4
opCode = msgChannelExtendedData
}
c.writeMu.Lock()
packet := c.packetPool[extendedCode]
// We don't remove the buffer from packetPool, so
// WriteExtended calls from different goroutines will be
// flagged as errors by the race detector.
c.writeMu.Unlock()
for len(data) > 0 {
space := min(c.maxRemotePayload, len(data))
if space, err = c.remoteWin.reserve(space); err != nil {
return n, err
}
if want := headerLength + space; uint32(cap(packet)) < want {
packet = make([]byte, want)
} else {
packet = packet[:want]
}
todo := data[:space]
packet[0] = opCode
binary.BigEndian.PutUint32(packet[1:], c.remoteId)
if extendedCode > 0 {
binary.BigEndian.PutUint32(packet[5:], uint32(extendedCode))
}
binary.BigEndian.PutUint32(packet[headerLength-4:], uint32(len(todo)))
copy(packet[headerLength:], todo)
if err = c.writePacket(packet); err != nil {
return n, err
}
n += len(todo)
data = data[len(todo):]
}
c.writeMu.Lock()
c.packetPool[extendedCode] = packet
c.writeMu.Unlock()
return n, err
}
func (c *channel) handleData(packet []byte) error {
headerLen := 9
isExtendedData := packet[0] == msgChannelExtendedData
if isExtendedData {
headerLen = 13
}
if len(packet) < headerLen {
// malformed data packet
return parseError(packet[0])
}
var extended uint32
if isExtendedData {
extended = binary.BigEndian.Uint32(packet[5:])
}
length := binary.BigEndian.Uint32(packet[headerLen-4 : headerLen])
if length == 0 {
return nil
}
if length > c.maxIncomingPayload {
// TODO(hanwen): should send Disconnect?
return errors.New("ssh: incoming packet exceeds maximum payload size")
}
data := packet[headerLen:]
if length != uint32(len(data)) {
return errors.New("ssh: wrong packet length")
}
c.windowMu.Lock()
if c.myWindow < length {
c.windowMu.Unlock()
// TODO(hanwen): should send Disconnect with reason?
return errors.New("ssh: remote side wrote too much")
}
c.myWindow -= length
c.windowMu.Unlock()
if extended == 1 {
c.extPending.write(data)
} else if extended > 0 {
// discard other extended data.
} else {
c.pending.write(data)
}
return nil
}
func (c *channel) adjustWindow(n uint32) error {
c.windowMu.Lock()
// Since myWindow is managed on our side, and can never exceed
// the initial window setting, we don't worry about overflow.
c.myWindow += uint32(n)
c.windowMu.Unlock()
return c.sendMessage(windowAdjustMsg{
AdditionalBytes: uint32(n),
})
}
func (c *channel) ReadExtended(data []byte, extended uint32) (n int, err error) {
switch extended {
case 1:
n, err = c.extPending.Read(data)
case 0:
n, err = c.pending.Read(data)
default:
return 0, fmt.Errorf("ssh: extended code %d unimplemented", extended)
}
if n > 0 {
err = c.adjustWindow(uint32(n))
// sendWindowAdjust can return io.EOF if the remote
// peer has closed the connection, however we want to
// defer forwarding io.EOF to the caller of Read until
// the buffer has been drained.
if n > 0 && err == io.EOF {
err = nil
}
}
return n, err
}
func (c *channel) close() {
c.pending.eof()
c.extPending.eof()
close(c.msg)
close(c.incomingRequests)
c.writeMu.Lock()
// This is not necesary for a normal channel teardown, but if
// there was another error, it is.
c.sentClose = true
c.writeMu.Unlock()
// Unblock writers.
c.remoteWin.close()
}
// responseMessageReceived is called when a success or failure message is
// received on a channel to check that such a message is reasonable for the
// given channel.
func (c *channel) responseMessageReceived() error {
if c.direction == channelInbound {
return errors.New("ssh: channel response message received on inbound channel")
}
if c.decided {
return errors.New("ssh: duplicate response received for channel")
}
c.decided = true
return nil
}
func (c *channel) handlePacket(packet []byte) error {
switch packet[0] {
case msgChannelData, msgChannelExtendedData:
return c.handleData(packet)
case msgChannelClose:
c.sendMessage(channelCloseMsg{PeersId: c.remoteId})
c.mux.chanList.remove(c.localId)
c.close()
return nil
case msgChannelEOF:
// RFC 4254 is mute on how EOF affects dataExt messages but
// it is logical to signal EOF at the same time.
c.extPending.eof()
c.pending.eof()
return nil
}
decoded, err := decode(packet)
if err != nil {
return err
}
switch msg := decoded.(type) {
case *channelOpenFailureMsg:
if err := c.responseMessageReceived(); err != nil {
return err
}
c.mux.chanList.remove(msg.PeersId)
c.msg <- msg
case *channelOpenConfirmMsg:
if err := c.responseMessageReceived(); err != nil {
return err
}
if msg.MaxPacketSize < minPacketLength || msg.MaxPacketSize > 1<<31 {
return fmt.Errorf("ssh: invalid MaxPacketSize %d from peer", msg.MaxPacketSize)
}
c.remoteId = msg.MyId
c.maxRemotePayload = msg.MaxPacketSize
c.remoteWin.add(msg.MyWindow)
c.msg <- msg
case *windowAdjustMsg:
if !c.remoteWin.add(msg.AdditionalBytes) {
return fmt.Errorf("ssh: invalid window update for %d bytes", msg.AdditionalBytes)
}
case *channelRequestMsg:
req := Request{
Type: msg.Request,
WantReply: msg.WantReply,
Payload: msg.RequestSpecificData,
ch: c,
}
c.incomingRequests <- &req
default:
c.msg <- msg
}
return nil
}
func (m *mux) newChannel(chanType string, direction channelDirection, extraData []byte) *channel {
ch := &channel{
remoteWin: window{Cond: newCond()},
myWindow: channelWindowSize,
pending: newBuffer(),
extPending: newBuffer(),
direction: direction,
incomingRequests: make(chan *Request, 16),
msg: make(chan interface{}, 16),
chanType: chanType,
extraData: extraData,
mux: m,
packetPool: make(map[uint32][]byte),
}
ch.localId = m.chanList.add(ch)
return ch
}
var errUndecided = errors.New("ssh: must Accept or Reject channel")
var errDecidedAlready = errors.New("ssh: can call Accept or Reject only once")
type extChannel struct {
code uint32
ch *channel
}
func (e *extChannel) Write(data []byte) (n int, err error) {
return e.ch.WriteExtended(data, e.code)
}
func (e *extChannel) Read(data []byte) (n int, err error) {
return e.ch.ReadExtended(data, e.code)
}
func (c *channel) Accept() (Channel, <-chan *Request, error) {
if c.decided {
return nil, nil, errDecidedAlready
}
c.maxIncomingPayload = channelMaxPacket
confirm := channelOpenConfirmMsg{
PeersId: c.remoteId,
MyId: c.localId,
MyWindow: c.myWindow,
MaxPacketSize: c.maxIncomingPayload,
}
c.decided = true
if err := c.sendMessage(confirm); err != nil {
return nil, nil, err
}
return c, c.incomingRequests, nil
}
func (ch *channel) Reject(reason RejectionReason, message string) error {
if ch.decided {
return errDecidedAlready
}
reject := channelOpenFailureMsg{
PeersId: ch.remoteId,
Reason: reason,
Message: message,
Language: "en",
}
ch.decided = true
return ch.sendMessage(reject)
}
func (ch *channel) Read(data []byte) (int, error) {
if !ch.decided {
return 0, errUndecided
}
return ch.ReadExtended(data, 0)
}
func (ch *channel) Write(data []byte) (int, error) {
if !ch.decided {
return 0, errUndecided
}
return ch.WriteExtended(data, 0)
}
func (ch *channel) CloseWrite() error {
if !ch.decided {
return errUndecided
}
ch.sentEOF = true
return ch.sendMessage(channelEOFMsg{
PeersId: ch.remoteId})
}
func (ch *channel) Close() error {
if !ch.decided {
return errUndecided
}
return ch.sendMessage(channelCloseMsg{
PeersId: ch.remoteId})
}
// Extended returns an io.ReadWriter that sends and receives data on the given,
// SSH extended stream. Such streams are used, for example, for stderr.
func (ch *channel) Extended(code uint32) io.ReadWriter {
if !ch.decided {
return nil
}
return &extChannel{code, ch}
}
func (ch *channel) Stderr() io.ReadWriter {
return ch.Extended(1)
}
func (ch *channel) SendRequest(name string, wantReply bool, payload []byte) (bool, error) {
if !ch.decided {
return false, errUndecided
}
if wantReply {
ch.sentRequestMu.Lock()
defer ch.sentRequestMu.Unlock()
}
msg := channelRequestMsg{
PeersId: ch.remoteId,
Request: name,
WantReply: wantReply,
RequestSpecificData: payload,
}
if err := ch.sendMessage(msg); err != nil {
return false, err
}
if wantReply {
m, ok := (<-ch.msg)
if !ok {
return false, io.EOF
}
switch m.(type) {
case *channelRequestFailureMsg:
return false, nil
case *channelRequestSuccessMsg:
return true, nil
default:
return false, fmt.Errorf("ssh: unexpected response to channel request: %#v", m)
}
}
return false, nil
}
// ackRequest either sends an ack or nack to the channel request.
func (ch *channel) ackRequest(ok bool) error {
if !ch.decided {
return errUndecided
}
var msg interface{}
if !ok {
msg = channelRequestFailureMsg{
PeersId: ch.remoteId,
}
} else {
msg = channelRequestSuccessMsg{
PeersId: ch.remoteId,
}
}
return ch.sendMessage(msg)
}
func (ch *channel) ChannelType() string {
return ch.chanType
}
func (ch *channel) ExtraData() []byte {
return ch.extraData
}

549
modules/crypto/ssh/cipher.go

@ -1,549 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"crypto/aes"
"crypto/cipher"
"crypto/rc4"
"crypto/subtle"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
"io/ioutil"
)
const (
packetSizeMultiple = 16 // TODO(huin) this should be determined by the cipher.
// RFC 4253 section 6.1 defines a minimum packet size of 32768 that implementations
// MUST be able to process (plus a few more kilobytes for padding and mac). The RFC
// indicates implementations SHOULD be able to handle larger packet sizes, but then
// waffles on about reasonable limits.
//
// OpenSSH caps their maxPacket at 256kB so we choose to do
// the same. maxPacket is also used to ensure that uint32
// length fields do not overflow, so it should remain well
// below 4G.
maxPacket = 256 * 1024
)
// noneCipher implements cipher.Stream and provides no encryption. It is used
// by the transport before the first key-exchange.
type noneCipher struct{}
func (c noneCipher) XORKeyStream(dst, src []byte) {
copy(dst, src)
}
func newAESCTR(key, iv []byte) (cipher.Stream, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewCTR(c, iv), nil
}
func newRC4(key, iv []byte) (cipher.Stream, error) {
return rc4.NewCipher(key)
}
type streamCipherMode struct {
keySize int
ivSize int
skip int
createFunc func(key, iv []byte) (cipher.Stream, error)
}
func (c *streamCipherMode) createStream(key, iv []byte) (cipher.Stream, error) {
if len(key) < c.keySize {
panic("ssh: key length too small for cipher")
}
if len(iv) < c.ivSize {
panic("ssh: iv too small for cipher")
}
stream, err := c.createFunc(key[:c.keySize], iv[:c.ivSize])
if err != nil {
return nil, err
}
var streamDump []byte
if c.skip > 0 {
streamDump = make([]byte, 512)
}
for remainingToDump := c.skip; remainingToDump > 0; {
dumpThisTime := remainingToDump
if dumpThisTime > len(streamDump) {
dumpThisTime = len(streamDump)
}
stream.XORKeyStream(streamDump[:dumpThisTime], streamDump[:dumpThisTime])
remainingToDump -= dumpThisTime
}
return stream, nil
}
// cipherModes documents properties of supported ciphers. Ciphers not included
// are not supported and will not be negotiated, even if explicitly requested in
// ClientConfig.Crypto.Ciphers.
var cipherModes = map[string]*streamCipherMode{
// Ciphers from RFC4344, which introduced many CTR-based ciphers. Algorithms
// are defined in the order specified in the RFC.
"aes128-ctr": {16, aes.BlockSize, 0, newAESCTR},
"aes192-ctr": {24, aes.BlockSize, 0, newAESCTR},
"aes256-ctr": {32, aes.BlockSize, 0, newAESCTR},
// Ciphers from RFC4345, which introduces security-improved arcfour ciphers.
// They are defined in the order specified in the RFC.
"arcfour128": {16, 0, 1536, newRC4},
"arcfour256": {32, 0, 1536, newRC4},
// Cipher defined in RFC 4253, which describes SSH Transport Layer Protocol.
// Note that this cipher is not safe, as stated in RFC 4253: "Arcfour (and
// RC4) has problems with weak keys, and should be used with caution."
// RFC4345 introduces improved versions of Arcfour.
"arcfour": {16, 0, 0, newRC4},
// AES-GCM is not a stream cipher, so it is constructed with a
// special case. If we add any more non-stream ciphers, we
// should invest a cleaner way to do this.
gcmCipherID: {16, 12, 0, nil},
// insecure cipher, see http://www.isg.rhul.ac.uk/~kp/SandPfinal.pdf
// uncomment below to enable it.
// aes128cbcID: {16, aes.BlockSize, 0, nil},
}
// prefixLen is the length of the packet prefix that contains the packet length
// and number of padding bytes.
const prefixLen = 5
// streamPacketCipher is a packetCipher using a stream cipher.
type streamPacketCipher struct {
mac hash.Hash
cipher cipher.Stream
// The following members are to avoid per-packet allocations.
prefix [prefixLen]byte
seqNumBytes [4]byte
padding [2 * packetSizeMultiple]byte
packetData []byte
macResult []byte
}
// readPacket reads and decrypt a single packet from the reader argument.
func (s *streamPacketCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) {
if _, err := io.ReadFull(r, s.prefix[:]); err != nil {
return nil, err
}
s.cipher.XORKeyStream(s.prefix[:], s.prefix[:])
length := binary.BigEndian.Uint32(s.prefix[0:4])
paddingLength := uint32(s.prefix[4])
var macSize uint32
if s.mac != nil {
s.mac.Reset()
binary.BigEndian.PutUint32(s.seqNumBytes[:], seqNum)
s.mac.Write(s.seqNumBytes[:])
s.mac.Write(s.prefix[:])
macSize = uint32(s.mac.Size())
}
if length <= paddingLength+1 {
return nil, errors.New("ssh: invalid packet length, packet too small")
}
if length > maxPacket {
return nil, errors.New("ssh: invalid packet length, packet too large")
}
// the maxPacket check above ensures that length-1+macSize
// does not overflow.
if uint32(cap(s.packetData)) < length-1+macSize {
s.packetData = make([]byte, length-1+macSize)
} else {
s.packetData = s.packetData[:length-1+macSize]
}
if _, err := io.ReadFull(r, s.packetData); err != nil {
return nil, err
}
mac := s.packetData[length-1:]
data := s.packetData[:length-1]
s.cipher.XORKeyStream(data, data)
if s.mac != nil {
s.mac.Write(data)
s.macResult = s.mac.Sum(s.macResult[:0])
if subtle.ConstantTimeCompare(s.macResult, mac) != 1 {
return nil, errors.New("ssh: MAC failure")
}
}
return s.packetData[:length-paddingLength-1], nil
}
// writePacket encrypts and sends a packet of data to the writer argument
func (s *streamPacketCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, packet []byte) error {
if len(packet) > maxPacket {
return errors.New("ssh: packet too large")
}
paddingLength := packetSizeMultiple - (prefixLen+len(packet))%packetSizeMultiple
if paddingLength < 4 {
paddingLength += packetSizeMultiple
}
length := len(packet) + 1 + paddingLength
binary.BigEndian.PutUint32(s.prefix[:], uint32(length))
s.prefix[4] = byte(paddingLength)
padding := s.padding[:paddingLength]
if _, err := io.ReadFull(rand, padding); err != nil {
return err
}
if s.mac != nil {
s.mac.Reset()
binary.BigEndian.PutUint32(s.seqNumBytes[:], seqNum)
s.mac.Write(s.seqNumBytes[:])
s.mac.Write(s.prefix[:])
s.mac.Write(packet)
s.mac.Write(padding)
}
s.cipher.XORKeyStream(s.prefix[:], s.prefix[:])
s.cipher.XORKeyStream(packet, packet)
s.cipher.XORKeyStream(padding, padding)
if _, err := w.Write(s.prefix[:]); err != nil {
return err
}
if _, err := w.Write(packet); err != nil {
return err
}
if _, err := w.Write(padding); err != nil {
return err
}
if s.mac != nil {
s.macResult = s.mac.Sum(s.macResult[:0])
if _, err := w.Write(s.macResult); err != nil {
return err
}
}
return nil
}
type gcmCipher struct {
aead cipher.AEAD
prefix [4]byte
iv []byte
buf []byte
}
func newGCMCipher(iv, key, macKey []byte) (packetCipher, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
return &gcmCipher{
aead: aead,
iv: iv,
}, nil
}
const gcmTagSize = 16
func (c *gcmCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, packet []byte) error {
// Pad out to multiple of 16 bytes. This is different from the
// stream cipher because that encrypts the length too.
padding := byte(packetSizeMultiple - (1+len(packet))%packetSizeMultiple)
if padding < 4 {
padding += packetSizeMultiple
}
length := uint32(len(packet) + int(padding) + 1)
binary.BigEndian.PutUint32(c.prefix[:], length)
if _, err := w.Write(c.prefix[:]); err != nil {
return err
}
if cap(c.buf) < int(length) {
c.buf = make([]byte, length)
} else {
c.buf = c.buf[:length]
}
c.buf[0] = padding
copy(c.buf[1:], packet)
if _, err := io.ReadFull(rand, c.buf[1+len(packet):]); err != nil {
return err
}
c.buf = c.aead.Seal(c.buf[:0], c.iv, c.buf, c.prefix[:])
if _, err := w.Write(c.buf); err != nil {
return err
}
c.incIV()
return nil
}
func (c *gcmCipher) incIV() {
for i := 4 + 7; i >= 4; i-- {
c.iv[i]++
if c.iv[i] != 0 {
break
}
}
}
func (c *gcmCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) {
if _, err := io.ReadFull(r, c.prefix[:]); err != nil {
return nil, err
}
length := binary.BigEndian.Uint32(c.prefix[:])
if length > maxPacket {
return nil, errors.New("ssh: max packet length exceeded.")
}
if cap(c.buf) < int(length+gcmTagSize) {
c.buf = make([]byte, length+gcmTagSize)
} else {
c.buf = c.buf[:length+gcmTagSize]
}
if _, err := io.ReadFull(r, c.buf); err != nil {
return nil, err
}
plain, err := c.aead.Open(c.buf[:0], c.iv, c.buf, c.prefix[:])
if err != nil {
return nil, err
}
c.incIV()
padding := plain[0]
if padding < 4 || padding >= 20 {
return nil, fmt.Errorf("ssh: illegal padding %d", padding)
}
if int(padding+1) >= len(plain) {
return nil, fmt.Errorf("ssh: padding %d too large", padding)
}
plain = plain[1 : length-uint32(padding)]
return plain, nil
}
// cbcCipher implements aes128-cbc cipher defined in RFC 4253 section 6.1
type cbcCipher struct {
mac hash.Hash
macSize uint32
decrypter cipher.BlockMode
encrypter cipher.BlockMode
// The following members are to avoid per-packet allocations.
seqNumBytes [4]byte
packetData []byte
macResult []byte
// Amount of data we should still read to hide which
// verification error triggered.
oracleCamouflage uint32
}
func newAESCBCCipher(iv, key, macKey []byte, algs directionAlgorithms) (packetCipher, error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cbc := &cbcCipher{
mac: macModes[algs.MAC].new(macKey),
decrypter: cipher.NewCBCDecrypter(c, iv),
encrypter: cipher.NewCBCEncrypter(c, iv),
packetData: make([]byte, 1024),
}
if cbc.mac != nil {
cbc.macSize = uint32(cbc.mac.Size())
}
return cbc, nil
}
func maxUInt32(a, b int) uint32 {
if a > b {
return uint32(a)
}
return uint32(b)
}
const (
cbcMinPacketSizeMultiple = 8
cbcMinPacketSize = 16
cbcMinPaddingSize = 4
)
// cbcError represents a verification error that may leak information.
type cbcError string
func (e cbcError) Error() string { return string(e) }
func (c *cbcCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) {
p, err := c.readPacketLeaky(seqNum, r)
if err != nil {
if _, ok := err.(cbcError); ok {
// Verification error: read a fixed amount of
// data, to make distinguishing between
// failing MAC and failing length check more
// difficult.
io.CopyN(ioutil.Discard, r, int64(c.oracleCamouflage))
}
}
return p, err
}
func (c *cbcCipher) readPacketLeaky(seqNum uint32, r io.Reader) ([]byte, error) {
blockSize := c.decrypter.BlockSize()
// Read the header, which will include some of the subsequent data in the
// case of block ciphers - this is copied back to the payload later.
// How many bytes of payload/padding will be read with this first read.
firstBlockLength := uint32((prefixLen + blockSize - 1) / blockSize * blockSize)
firstBlock := c.packetData[:firstBlockLength]
if _, err := io.ReadFull(r, firstBlock); err != nil {
return nil, err
}
c.oracleCamouflage = maxPacket + 4 + c.macSize - firstBlockLength
c.decrypter.CryptBlocks(firstBlock, firstBlock)
length := binary.BigEndian.Uint32(firstBlock[:4])
if length > maxPacket {
return nil, cbcError("ssh: packet too large")
}
if length+4 < maxUInt32(cbcMinPacketSize, blockSize) {
// The minimum size of a packet is 16 (or the cipher block size, whichever
// is larger) bytes.
return nil, cbcError("ssh: packet too small")
}
// The length of the packet (including the length field but not the MAC) must
// be a multiple of the block size or 8, whichever is larger.
if (length+4)%maxUInt32(cbcMinPacketSizeMultiple, blockSize) != 0 {
return nil, cbcError("ssh: invalid packet length multiple")
}
paddingLength := uint32(firstBlock[4])
if paddingLength < cbcMinPaddingSize || length <= paddingLength+1 {
return nil, cbcError("ssh: invalid packet length")
}
// Positions within the c.packetData buffer:
macStart := 4 + length
paddingStart := macStart - paddingLength
// Entire packet size, starting before length, ending at end of mac.
entirePacketSize := macStart + c.macSize
// Ensure c.packetData is large enough for the entire packet data.
if uint32(cap(c.packetData)) < entirePacketSize {
// Still need to upsize and copy, but this should be rare at runtime, only
// on upsizing the packetData buffer.
c.packetData = make([]byte, entirePacketSize)
copy(c.packetData, firstBlock)
} else {
c.packetData = c.packetData[:entirePacketSize]
}
if n, err := io.ReadFull(r, c.packetData[firstBlockLength:]); err != nil {
return nil, err
} else {
c.oracleCamouflage -= uint32(n)
}
remainingCrypted := c.packetData[firstBlockLength:macStart]
c.decrypter.CryptBlocks(remainingCrypted, remainingCrypted)
mac := c.packetData[macStart:]
if c.mac != nil {
c.mac.Reset()
binary.BigEndian.PutUint32(c.seqNumBytes[:], seqNum)
c.mac.Write(c.seqNumBytes[:])
c.mac.Write(c.packetData[:macStart])
c.macResult = c.mac.Sum(c.macResult[:0])
if subtle.ConstantTimeCompare(c.macResult, mac) != 1 {
return nil, cbcError("ssh: MAC failure")
}
}
return c.packetData[prefixLen:paddingStart], nil
}
func (c *cbcCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, packet []byte) error {
effectiveBlockSize := maxUInt32(cbcMinPacketSizeMultiple, c.encrypter.BlockSize())
// Length of encrypted portion of the packet (header, payload, padding).
// Enforce minimum padding and packet size.
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize)
// Enforce block size.
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
length := encLength - 4
paddingLength := int(length) - (1 + len(packet))
// Overall buffer contains: header, payload, padding, mac.
// Space for the MAC is reserved in the capacity but not the slice length.
bufferSize := encLength + c.macSize
if uint32(cap(c.packetData)) < bufferSize {
c.packetData = make([]byte, encLength, bufferSize)
} else {
c.packetData = c.packetData[:encLength]
}
p := c.packetData
// Packet header.
binary.BigEndian.PutUint32(p, length)
p = p[4:]
p[0] = byte(paddingLength)
// Payload.
p = p[1:]
copy(p, packet)
// Padding.
p = p[len(packet):]
if _, err := io.ReadFull(rand, p); err != nil {
return err
}
if c.mac != nil {
c.mac.Reset()
binary.BigEndian.PutUint32(c.seqNumBytes[:], seqNum)
c.mac.Write(c.seqNumBytes[:])
c.mac.Write(c.packetData)
// The MAC is now appended into the capacity reserved for it earlier.
c.packetData = c.mac.Sum(c.packetData)
}
c.encrypter.CryptBlocks(c.packetData[:encLength], c.packetData[:encLength])
if _, err := w.Write(c.packetData); err != nil {
return err
}
return nil
}

127
modules/crypto/ssh/cipher_test.go

@ -1,127 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto"
"crypto/aes"
"crypto/rand"
"testing"
)
func TestDefaultCiphersExist(t *testing.T) {
for _, cipherAlgo := range supportedCiphers {
if _, ok := cipherModes[cipherAlgo]; !ok {
t.Errorf("default cipher %q is unknown", cipherAlgo)
}
}
}
func TestPacketCiphers(t *testing.T) {
// Still test aes128cbc cipher althought it's commented out.
cipherModes[aes128cbcID] = &streamCipherMode{16, aes.BlockSize, 0, nil}
defer delete(cipherModes, aes128cbcID)
for cipher := range cipherModes {
kr := &kexResult{Hash: crypto.SHA1}
algs := directionAlgorithms{
Cipher: cipher,
MAC: "hmac-sha1",
Compression: "none",
}
client, err := newPacketCipher(clientKeys, algs, kr)
if err != nil {
t.Errorf("newPacketCipher(client, %q): %v", cipher, err)
continue
}
server, err := newPacketCipher(clientKeys, algs, kr)
if err != nil {
t.Errorf("newPacketCipher(client, %q): %v", cipher, err)
continue
}
want := "bla bla"
input := []byte(want)
buf := &bytes.Buffer{}
if err := client.writePacket(0, buf, rand.Reader, input); err != nil {
t.Errorf("writePacket(%q): %v", cipher, err)
continue
}
packet, err := server.readPacket(0, buf)
if err != nil {
t.Errorf("readPacket(%q): %v", cipher, err)
continue
}
if string(packet) != want {
t.Errorf("roundtrip(%q): got %q, want %q", cipher, packet, want)
}
}
}
func TestCBCOracleCounterMeasure(t *testing.T) {
cipherModes[aes128cbcID] = &streamCipherMode{16, aes.BlockSize, 0, nil}
defer delete(cipherModes, aes128cbcID)
kr := &kexResult{Hash: crypto.SHA1}
algs := directionAlgorithms{
Cipher: aes128cbcID,
MAC: "hmac-sha1",
Compression: "none",
}
client, err := newPacketCipher(clientKeys, algs, kr)
if err != nil {
t.Fatalf("newPacketCipher(client): %v", err)
}
want := "bla bla"
input := []byte(want)
buf := &bytes.Buffer{}
if err := client.writePacket(0, buf, rand.Reader, input); err != nil {
t.Errorf("writePacket: %v", err)
}
packetSize := buf.Len()
buf.Write(make([]byte, 2*maxPacket))
// We corrupt each byte, but this usually will only test the
// 'packet too large' or 'MAC failure' cases.
lastRead := -1
for i := 0; i < packetSize; i++ {
server, err := newPacketCipher(clientKeys, algs, kr)
if err != nil {
t.Fatalf("newPacketCipher(client): %v", err)
}
fresh := &bytes.Buffer{}
fresh.Write(buf.Bytes())
fresh.Bytes()[i] ^= 0x01
before := fresh.Len()
_, err = server.readPacket(0, fresh)
if err == nil {
t.Errorf("corrupt byte %d: readPacket succeeded ", i)
continue
}
if _, ok := err.(cbcError); !ok {
t.Errorf("corrupt byte %d: got %v (%T), want cbcError", i, err, err)
continue
}
after := fresh.Len()
bytesRead := before - after
if bytesRead < maxPacket {
t.Errorf("corrupt byte %d: read %d bytes, want more than %d", i, bytesRead, maxPacket)
continue
}
if i > 0 && bytesRead != lastRead {
t.Errorf("corrupt byte %d: read %d bytes, want %d bytes read", i, bytesRead, lastRead)
}
lastRead = bytesRead
}
}

213
modules/crypto/ssh/client.go

@ -1,213 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"errors"
"fmt"
"net"
"sync"
)
// Client implements a traditional SSH client that supports shells,
// subprocesses, port forwarding and tunneled dialing.
type Client struct {
Conn
forwards forwardList // forwarded tcpip connections from the remote side
mu sync.Mutex
channelHandlers map[string]chan NewChannel
}
// HandleChannelOpen returns a channel on which NewChannel requests
// for the given type are sent. If the type already is being handled,
// nil is returned. The channel is closed when the connection is closed.
func (c *Client) HandleChannelOpen(channelType string) <-chan NewChannel {
c.mu.Lock()
defer c.mu.Unlock()
if c.channelHandlers == nil {
// The SSH channel has been closed.
c := make(chan NewChannel)
close(c)
return c
}
ch := c.channelHandlers[channelType]
if ch != nil {
return nil
}
ch = make(chan NewChannel, 16)
c.channelHandlers[channelType] = ch
return ch
}
// NewClient creates a Client on top of the given connection.
func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client {
conn := &Client{
Conn: c,
channelHandlers: make(map[string]chan NewChannel, 1),
}
go conn.handleGlobalRequests(reqs)
go conn.handleChannelOpens(chans)
go func() {
conn.Wait()
conn.forwards.closeAll()
}()
go conn.forwards.handleChannels(conn.HandleChannelOpen("forwarded-tcpip"))
return conn
}
// NewClientConn establishes an authenticated SSH connection using c
// as the underlying transport. The Request and NewChannel channels
// must be serviced or the connection will hang.
func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) {
fullConf := *config
fullConf.SetDefaults()
conn := &connection{
sshConn: sshConn{conn: c},
}
if err := conn.clientHandshake(addr, &fullConf); err != nil {
c.Close()
return nil, nil, nil, fmt.Errorf("ssh: handshake failed: %v", err)
}
conn.mux = newMux(conn.transport)
return conn, conn.mux.incomingChannels, conn.mux.incomingRequests, nil
}
// clientHandshake performs the client side key exchange. See RFC 4253 Section
// 7.
func (c *connection) clientHandshake(dialAddress string, config *ClientConfig) error {
if config.ClientVersion != "" {
c.clientVersion = []byte(config.ClientVersion)
} else {
c.clientVersion = []byte(packageVersion)
}
var err error
c.serverVersion, err = exchangeVersions(c.sshConn.conn, c.clientVersion)
if err != nil {
return err
}
c.transport = newClientTransport(
newTransport(c.sshConn.conn, config.Rand, true /* is client */),
c.clientVersion, c.serverVersion, config, dialAddress, c.sshConn.RemoteAddr())
if err := c.transport.requestKeyChange(); err != nil {
return err
}
if packet, err := c.transport.readPacket(); err != nil {
return err
} else if packet[0] != msgNewKeys {
return unexpectedMessageError(msgNewKeys, packet[0])
}
// We just did the key change, so the session ID is established.
c.sessionID = c.transport.getSessionID()
return c.clientAuthenticate(config)
}
// verifyHostKeySignature verifies the host key obtained in the key
// exchange.
func verifyHostKeySignature(hostKey PublicKey, result *kexResult) error {
sig, rest, ok := parseSignatureBody(result.Signature)
if len(rest) > 0 || !ok {
return errors.New("ssh: signature parse error")
}
return hostKey.Verify(result.H, sig)
}
// NewSession opens a new Session for this client. (A session is a remote
// execution of a program.)
func (c *Client) NewSession() (*Session, error) {
ch, in, err := c.OpenChannel("session", nil)
if err != nil {
return nil, err
}
return newSession(ch, in)
}
func (c *Client) handleGlobalRequests(incoming <-chan *Request) {
for r := range incoming {
// This handles keepalive messages and matches
// the behaviour of OpenSSH.
r.Reply(false, nil)
}
}
// handleChannelOpens channel open messages from the remote side.
func (c *Client) handleChannelOpens(in <-chan NewChannel) {
for ch := range in {
c.mu.Lock()
handler := c.channelHandlers[ch.ChannelType()]
c.mu.Unlock()
if handler != nil {
handler <- ch
} else {
ch.Reject(UnknownChannelType, fmt.Sprintf("unknown channel type: %v", ch.ChannelType()))
}
}
c.mu.Lock()
for _, ch := range c.channelHandlers {
close(ch)
}
c.channelHandlers = nil
c.mu.Unlock()
}
// Dial starts a client connection to the given SSH server. It is a
// convenience function that connects to the given network address,
// initiates the SSH handshake, and then sets up a Client. For access
// to incoming channels and requests, use net.Dial with NewClientConn
// instead.
func Dial(network, addr string, config *ClientConfig) (*Client, error) {
conn, err := net.Dial(network, addr)
if err != nil {
return nil, err
}
c, chans, reqs, err := NewClientConn(conn, addr, config)
if err != nil {
return nil, err
}
return NewClient(c, chans, reqs), nil
}
// A ClientConfig structure is used to configure a Client. It must not be
// modified after having been passed to an SSH function.
type ClientConfig struct {
// Config contains configuration that is shared between clients and
// servers.
Config
// User contains the username to authenticate as.
User string
// Auth contains possible authentication methods to use with the
// server. Only the first instance of a particular RFC 4252 method will
// be used during authentication.
Auth []AuthMethod
// HostKeyCallback, if not nil, is called during the cryptographic
// handshake to validate the server's host key. A nil HostKeyCallback
// implies that all host keys are accepted.
HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
// ClientVersion contains the version identification string that will
// be used for the connection. If empty, a reasonable default is used.
ClientVersion string
// HostKeyAlgorithms lists the key types that the client will
// accept from the server as host key, in order of
// preference. If empty, a reasonable default is used. Any
// string returned from PublicKey.Type method may be used, or
// any of the CertAlgoXxxx and KeyAlgoXxxx constants.
HostKeyAlgorithms []string
}

441
modules/crypto/ssh/client_auth.go

@ -1,441 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"errors"
"fmt"
"io"
)
// clientAuthenticate authenticates with the remote server. See RFC 4252.
func (c *connection) clientAuthenticate(config *ClientConfig) error {
// initiate user auth session
if err := c.transport.writePacket(Marshal(&serviceRequestMsg{serviceUserAuth})); err != nil {
return err
}
packet, err := c.transport.readPacket()
if err != nil {
return err
}
var serviceAccept serviceAcceptMsg
if err := Unmarshal(packet, &serviceAccept); err != nil {
return err
}
// during the authentication phase the client first attempts the "none" method
// then any untried methods suggested by the server.
tried := make(map[string]bool)
var lastMethods []string
for auth := AuthMethod(new(noneAuth)); auth != nil; {
ok, methods, err := auth.auth(c.transport.getSessionID(), config.User, c.transport, config.Rand)
if err != nil {
return err
}
if ok {
// success
return nil
}
tried[auth.method()] = true
if methods == nil {
methods = lastMethods
}
lastMethods = methods
auth = nil
findNext:
for _, a := range config.Auth {
candidateMethod := a.method()
if tried[candidateMethod] {
continue
}
for _, meth := range methods {
if meth == candidateMethod {
auth = a
break findNext
}
}
}
}
return fmt.Errorf("ssh: unable to authenticate, attempted methods %v, no supported methods remain", keys(tried))
}
func keys(m map[string]bool) []string {
s := make([]string, 0, len(m))
for key := range m {
s = append(s, key)
}
return s
}
// An AuthMethod represents an instance of an RFC 4252 authentication method.
type AuthMethod interface {
// auth authenticates user over transport t.
// Returns true if authentication is successful.
// If authentication is not successful, a []string of alternative
// method names is returned. If the slice is nil, it will be ignored
// and the previous set of possible methods will be reused.
auth(session []byte, user string, p packetConn, rand io.Reader) (bool, []string, error)
// method returns the RFC 4252 method name.
method() string
}
// "none" authentication, RFC 4252 section 5.2.
type noneAuth int
func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
if err := c.writePacket(Marshal(&userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: "none",
})); err != nil {
return false, nil, err
}
return handleAuthResponse(c)
}
func (n *noneAuth) method() string {
return "none"
}
// passwordCallback is an AuthMethod that fetches the password through
// a function call, e.g. by prompting the user.
type passwordCallback func() (password string, err error)
func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
type passwordAuthMsg struct {
User string `sshtype:"50"`
Service string
Method string
Reply bool
Password string
}
pw, err := cb()
// REVIEW NOTE: is there a need to support skipping a password attempt?
// The program may only find out that the user doesn't have a password
// when prompting.
if err != nil {
return false, nil, err
}
if err := c.writePacket(Marshal(&passwordAuthMsg{
User: user,
Service: serviceSSH,
Method: cb.method(),
Reply: false,
Password: pw,
})); err != nil {
return false, nil, err
}
return handleAuthResponse(c)
}
func (cb passwordCallback) method() string {
return "password"
}
// Password returns an AuthMethod using the given password.
func Password(secret string) AuthMethod {
return passwordCallback(func() (string, error) { return secret, nil })
}
// PasswordCallback returns an AuthMethod that uses a callback for
// fetching a password.
func PasswordCallback(prompt func() (secret string, err error)) AuthMethod {
return passwordCallback(prompt)
}
type publickeyAuthMsg struct {
User string `sshtype:"50"`
Service string
Method string
// HasSig indicates to the receiver packet that the auth request is signed and
// should be used for authentication of the request.
HasSig bool
Algoname string
PubKey []byte
// Sig is tagged with "rest" so Marshal will exclude it during
// validateKey
Sig []byte `ssh:"rest"`
}
// publicKeyCallback is an AuthMethod that uses a set of key
// pairs for authentication.
type publicKeyCallback func() ([]Signer, error)
func (cb publicKeyCallback) method() string {
return "publickey"
}
func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
// Authentication is performed in two stages. The first stage sends an
// enquiry to test if each key is acceptable to the remote. The second
// stage attempts to authenticate with the valid keys obtained in the
// first stage.
signers, err := cb()
if err != nil {
return false, nil, err
}
var validKeys []Signer
for _, signer := range signers {
if ok, err := validateKey(signer.PublicKey(), user, c); ok {
validKeys = append(validKeys, signer)
} else {
if err != nil {
return false, nil, err
}
}
}
// methods that may continue if this auth is not successful.
var methods []string
for _, signer := range validKeys {
pub := signer.PublicKey()
pubKey := pub.Marshal()
sign, err := signer.Sign(rand, buildDataSignedForAuth(session, userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: cb.method(),
}, []byte(pub.Type()), pubKey))
if err != nil {
return false, nil, err
}
// manually wrap the serialized signature in a string
s := Marshal(sign)
sig := make([]byte, stringLength(len(s)))
marshalString(sig, s)
msg := publickeyAuthMsg{
User: user,
Service: serviceSSH,
Method: cb.method(),
HasSig: true,
Algoname: pub.Type(),
PubKey: pubKey,
Sig: sig,
}
p := Marshal(&msg)
if err := c.writePacket(p); err != nil {
return false, nil, err
}
var success bool
success, methods, err = handleAuthResponse(c)
if err != nil {
return false, nil, err
}
if success {
return success, methods, err
}
}
return false, methods, nil
}
// validateKey validates the key provided is acceptable to the server.
func validateKey(key PublicKey, user string, c packetConn) (bool, error) {
pubKey := key.Marshal()
msg := publickeyAuthMsg{
User: user,
Service: serviceSSH,
Method: "publickey",
HasSig: false,
Algoname: key.Type(),
PubKey: pubKey,
}
if err := c.writePacket(Marshal(&msg)); err != nil {
return false, err
}
return confirmKeyAck(key, c)
}
func confirmKeyAck(key PublicKey, c packetConn) (bool, error) {
pubKey := key.Marshal()
algoname := key.Type()
for {
packet, err := c.readPacket()
if err != nil {
return false, err
}
switch packet[0] {
case msgUserAuthBanner:
// TODO(gpaul): add callback to present the banner to the user
case msgUserAuthPubKeyOk:
var msg userAuthPubKeyOkMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, err
}
if msg.Algo != algoname || !bytes.Equal(msg.PubKey, pubKey) {
return false, nil
}
return true, nil
case msgUserAuthFailure:
return false, nil
default:
return false, unexpectedMessageError(msgUserAuthSuccess, packet[0])
}
}
}
// PublicKeys returns an AuthMethod that uses the given key
// pairs.
func PublicKeys(signers ...Signer) AuthMethod {
return publicKeyCallback(func() ([]Signer, error) { return signers, nil })
}
// PublicKeysCallback returns an AuthMethod that runs the given
// function to obtain a list of key pairs.
func PublicKeysCallback(getSigners func() (signers []Signer, err error)) AuthMethod {
return publicKeyCallback(getSigners)
}
// handleAuthResponse returns whether the preceding authentication request succeeded
// along with a list of remaining authentication methods to try next and
// an error if an unexpected response was received.
func handleAuthResponse(c packetConn) (bool, []string, error) {
for {
packet, err := c.readPacket()
if err != nil {
return false, nil, err
}
switch packet[0] {
case msgUserAuthBanner:
// TODO: add callback to present the banner to the user
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
}
return false, msg.Methods, nil
case msgUserAuthSuccess:
return true, nil, nil
case msgDisconnect:
return false, nil, io.EOF
default:
return false, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0])
}
}
}
// KeyboardInteractiveChallenge should print questions, optionally
// disabling echoing (e.g. for passwords), and return all the answers.
// Challenge may be called multiple times in a single session. After
// successful authentication, the server may send a challenge with no
// questions, for which the user and instruction messages should be
// printed. RFC 4256 section 3.3 details how the UI should behave for
// both CLI and GUI environments.
type KeyboardInteractiveChallenge func(user, instruction string, questions []string, echos []bool) (answers []string, err error)
// KeyboardInteractive returns a AuthMethod using a prompt/response
// sequence controlled by the server.
func KeyboardInteractive(challenge KeyboardInteractiveChallenge) AuthMethod {
return challenge
}
func (cb KeyboardInteractiveChallenge) method() string {
return "keyboard-interactive"
}
func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader) (bool, []string, error) {
type initiateMsg struct {
User string `sshtype:"50"`
Service string
Method string
Language string
Submethods string
}
if err := c.writePacket(Marshal(&initiateMsg{
User: user,
Service: serviceSSH,
Method: "keyboard-interactive",
})); err != nil {
return false, nil, err
}
for {
packet, err := c.readPacket()
if err != nil {
return false, nil, err
}
// like handleAuthResponse, but with less options.
switch packet[0] {
case msgUserAuthBanner:
// TODO: Print banners during userauth.
continue
case msgUserAuthInfoRequest:
// OK
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
}
return false, msg.Methods, nil
case msgUserAuthSuccess:
return true, nil, nil
default:
return false, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0])
}
var msg userAuthInfoRequestMsg
if err := Unmarshal(packet, &msg); err != nil {
return false, nil, err
}
// Manually unpack the prompt/echo pairs.
rest := msg.Prompts
var prompts []string
var echos []bool
for i := 0; i < int(msg.NumPrompts); i++ {
prompt, r, ok := parseString(rest)
if !ok || len(r) == 0 {
return false, nil, errors.New("ssh: prompt format error")
}
prompts = append(prompts, string(prompt))
echos = append(echos, r[0] != 0)
rest = r[1:]
}
if len(rest) != 0 {
return false, nil, errors.New("ssh: extra data following keyboard-interactive pairs")
}
answers, err := cb(msg.User, msg.Instruction, prompts, echos)
if err != nil {
return false, nil, err
}
if len(answers) != len(prompts) {
return false, nil, errors.New("ssh: not enough answers from keyboard-interactive callback")
}
responseLength := 1 + 4
for _, a := range answers {
responseLength += stringLength(len(a))
}
serialized := make([]byte, responseLength)
p := serialized
p[0] = msgUserAuthInfoResponse
p = p[1:]
p = marshalUint32(p, uint32(len(answers)))
for _, a := range answers {
p = marshalString(p, []byte(a))
}
if err := c.writePacket(serialized); err != nil {
return false, nil, err
}
}
}

393
modules/crypto/ssh/client_auth_test.go

@ -1,393 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"strings"
"testing"
)
type keyboardInteractive map[string]string
func (cr keyboardInteractive) Challenge(user string, instruction string, questions []string, echos []bool) ([]string, error) {
var answers []string
for _, q := range questions {
answers = append(answers, cr[q])
}
return answers, nil
}
// reused internally by tests
var clientPassword = "tiger"
// tryAuth runs a handshake with a given config against an SSH server
// with config serverConfig
func tryAuth(t *testing.T, config *ClientConfig) error {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
certChecker := CertChecker{
IsAuthority: func(k PublicKey) bool {
return bytes.Equal(k.Marshal(), testPublicKeys["ecdsa"].Marshal())
},
UserKeyFallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if conn.User() == "testuser" && bytes.Equal(key.Marshal(), testPublicKeys["rsa"].Marshal()) {
return nil, nil
}
return nil, fmt.Errorf("pubkey for %q not acceptable", conn.User())
},
IsRevoked: func(c *Certificate) bool {
return c.Serial == 666
},
}
serverConfig := &ServerConfig{
PasswordCallback: func(conn ConnMetadata, pass []byte) (*Permissions, error) {
if conn.User() == "testuser" && string(pass) == clientPassword {
return nil, nil
}
return nil, errors.New("password auth failed")
},
PublicKeyCallback: certChecker.Authenticate,
KeyboardInteractiveCallback: func(conn ConnMetadata, challenge KeyboardInteractiveChallenge) (*Permissions, error) {
ans, err := challenge("user",
"instruction",
[]string{"question1", "question2"},
[]bool{true, true})
if err != nil {
return nil, err
}
ok := conn.User() == "testuser" && ans[0] == "answer1" && ans[1] == "answer2"
if ok {
challenge("user", "motd", nil, nil)
return nil, nil
}
return nil, errors.New("keyboard-interactive failed")
},
AuthLogCallback: func(conn ConnMetadata, method string, err error) {
t.Logf("user %q, method %q: %v", conn.User(), method, err)
},
}
serverConfig.AddHostKey(testSigners["rsa"])
go newServer(c1, serverConfig)
_, _, _, err = NewClientConn(c2, "", config)
return err
}
func TestClientAuthPublicKey(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthMethodPassword(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
Password(clientPassword),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthMethodFallback(t *testing.T) {
var passwordCalled bool
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
PasswordCallback(
func() (string, error) {
passwordCalled = true
return "WRONG", nil
}),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
if passwordCalled {
t.Errorf("password auth tried before public-key auth.")
}
}
func TestAuthMethodWrongPassword(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
Password("wrong"),
PublicKeys(testSigners["rsa"]),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthMethodKeyboardInteractive(t *testing.T) {
answers := keyboardInteractive(map[string]string{
"question1": "answer1",
"question2": "answer2",
})
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
KeyboardInteractive(answers.Challenge),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}
func TestAuthMethodWrongKeyboardInteractive(t *testing.T) {
answers := keyboardInteractive(map[string]string{
"question1": "answer1",
"question2": "WRONG",
})
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
KeyboardInteractive(answers.Challenge),
},
}
if err := tryAuth(t, config); err == nil {
t.Fatalf("wrong answers should not have authenticated with KeyboardInteractive")
}
}
// the mock server will only authenticate ssh-rsa keys
func TestAuthMethodInvalidPublicKey(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["dsa"]),
},
}
if err := tryAuth(t, config); err == nil {
t.Fatalf("dsa private key should not have authenticated with rsa public key")
}
}
// the client should authenticate with the second key
func TestAuthMethodRSAandDSA(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["dsa"], testSigners["rsa"]),
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("client could not authenticate with rsa key: %v", err)
}
}
func TestClientHMAC(t *testing.T) {
for _, mac := range supportedMACs {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
},
Config: Config{
MACs: []string{mac},
},
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("client could not authenticate with mac algo %s: %v", mac, err)
}
}
}
// issue 4285.
func TestClientUnsupportedCipher(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(),
},
Config: Config{
Ciphers: []string{"aes128-cbc"}, // not currently supported
},
}
if err := tryAuth(t, config); err == nil {
t.Errorf("expected no ciphers in common")
}
}
func TestClientUnsupportedKex(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(),
},
Config: Config{
KeyExchanges: []string{"diffie-hellman-group-exchange-sha256"}, // not currently supported
},
}
if err := tryAuth(t, config); err == nil || !strings.Contains(err.Error(), "common algorithm") {
t.Errorf("got %v, expected 'common algorithm'", err)
}
}
func TestClientLoginCert(t *testing.T) {
cert := &Certificate{
Key: testPublicKeys["rsa"],
ValidBefore: CertTimeInfinity,
CertType: UserCert,
}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
certSigner, err := NewCertSigner(cert, testSigners["rsa"])
if err != nil {
t.Fatalf("NewCertSigner: %v", err)
}
clientConfig := &ClientConfig{
User: "user",
}
clientConfig.Auth = append(clientConfig.Auth, PublicKeys(certSigner))
t.Log("should succeed")
if err := tryAuth(t, clientConfig); err != nil {
t.Errorf("cert login failed: %v", err)
}
t.Log("corrupted signature")
cert.Signature.Blob[0]++
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with corrupted sig")
}
t.Log("revoked")
cert.Serial = 666
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("revoked cert login succeeded")
}
cert.Serial = 1
t.Log("sign with wrong key")
cert.SignCert(rand.Reader, testSigners["dsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with non-authoritive key")
}
t.Log("host cert")
cert.CertType = HostCert
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with wrong type")
}
cert.CertType = UserCert
t.Log("principal specified")
cert.ValidPrincipals = []string{"user"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err != nil {
t.Errorf("cert login failed: %v", err)
}
t.Log("wrong principal specified")
cert.ValidPrincipals = []string{"fred"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with wrong principal")
}
cert.ValidPrincipals = nil
t.Log("added critical option")
cert.CriticalOptions = map[string]string{"root-access": "yes"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login passed with unrecognized critical option")
}
t.Log("allowed source address")
cert.CriticalOptions = map[string]string{"source-address": "127.0.0.42/24"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err != nil {
t.Errorf("cert login with source-address failed: %v", err)
}
t.Log("disallowed source address")
cert.CriticalOptions = map[string]string{"source-address": "127.0.0.42"}
cert.SignCert(rand.Reader, testSigners["ecdsa"])
if err := tryAuth(t, clientConfig); err == nil {
t.Errorf("cert login with source-address succeeded")
}
}
func testPermissionsPassing(withPermissions bool, t *testing.T) {
serverConfig := &ServerConfig{
PublicKeyCallback: func(conn ConnMetadata, key PublicKey) (*Permissions, error) {
if conn.User() == "nopermissions" {
return nil, nil
} else {
return &Permissions{}, nil
}
},
}
serverConfig.AddHostKey(testSigners["rsa"])
clientConfig := &ClientConfig{
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
},
}
if withPermissions {
clientConfig.User = "permissions"
} else {
clientConfig.User = "nopermissions"
}
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()
go NewClientConn(c2, "", clientConfig)
serverConn, err := newServer(c1, serverConfig)
if err != nil {
t.Fatal(err)
}
if p := serverConn.Permissions; (p != nil) != withPermissions {
t.Fatalf("withPermissions is %t, but Permissions object is %#v", withPermissions, p)
}
}
func TestPermissionsPassing(t *testing.T) {
testPermissionsPassing(true, t)
}
func TestNoPermissionsPassing(t *testing.T) {
testPermissionsPassing(false, t)
}

39
modules/crypto/ssh/client_test.go

@ -1,39 +0,0 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"net"
"testing"
)
func testClientVersion(t *testing.T, config *ClientConfig, expected string) {
clientConn, serverConn := net.Pipe()
defer clientConn.Close()
receivedVersion := make(chan string, 1)
go func() {
version, err := readVersion(serverConn)
if err != nil {
receivedVersion <- ""
} else {
receivedVersion <- string(version)
}
serverConn.Close()
}()
NewClientConn(clientConn, "", config)
actual := <-receivedVersion
if actual != expected {
t.Fatalf("got %s; want %s", actual, expected)
}
}
func TestCustomClientVersion(t *testing.T) {
version := "Test-Client-Version-0.0"
testClientVersion(t, &ClientConfig{ClientVersion: version}, version)
}
func TestDefaultClientVersion(t *testing.T) {
testClientVersion(t, &ClientConfig{}, packageVersion)
}

354
modules/crypto/ssh/common.go

@ -1,354 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"crypto"
"crypto/rand"
"fmt"
"io"
"sync"
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha512"
)
// These are string constants in the SSH protocol.
const (
compressionNone = "none"
serviceUserAuth = "ssh-userauth"
serviceSSH = "ssh-connection"
)
// supportedCiphers specifies the supported ciphers in preference order.
var supportedCiphers = []string{
"aes128-ctr", "aes192-ctr", "aes256-ctr",
"aes128-gcm@openssh.com",
"arcfour256", "arcfour128",
}
// supportedKexAlgos specifies the supported key-exchange algorithms in
// preference order.
var supportedKexAlgos = []string{
kexAlgoCurve25519SHA256,
// P384 and P521 are not constant-time yet, but since we don't
// reuse ephemeral keys, using them for ECDH should be OK.
kexAlgoECDH256, kexAlgoECDH384, kexAlgoECDH521,
kexAlgoDH14SHA1, kexAlgoDH1SHA1,
}
// supportedKexAlgos specifies the supported host-key algorithms (i.e. methods
// of authenticating servers) in preference order.
var supportedHostKeyAlgos = []string{
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01,
CertAlgoECDSA384v01, CertAlgoECDSA521v01,
KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521,
KeyAlgoRSA, KeyAlgoDSA,
}
// supportedMACs specifies a default set of MAC algorithms in preference order.
// This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed
// because they have reached the end of their useful life.
var supportedMACs = []string{
"hmac-sha2-256", "hmac-sha1", "hmac-sha1-96",
}
var supportedCompressions = []string{compressionNone}
// hashFuncs keeps the mapping of supported algorithms to their respective
// hashes needed for signature verification.
var hashFuncs = map[string]crypto.Hash{
KeyAlgoRSA: crypto.SHA1,
KeyAlgoDSA: crypto.SHA1,
KeyAlgoECDSA256: crypto.SHA256,
KeyAlgoECDSA384: crypto.SHA384,
KeyAlgoECDSA521: crypto.SHA512,
CertAlgoRSAv01: crypto.SHA1,
CertAlgoDSAv01: crypto.SHA1,
CertAlgoECDSA256v01: crypto.SHA256,
CertAlgoECDSA384v01: crypto.SHA384,
CertAlgoECDSA521v01: crypto.SHA512,
}
// unexpectedMessageError results when the SSH message that we received didn't
// match what we wanted.
func unexpectedMessageError(expected, got uint8) error {
return fmt.Errorf("ssh: unexpected message type %d (expected %d)", got, expected)
}
// parseError results from a malformed SSH message.
func parseError(tag uint8) error {
return fmt.Errorf("ssh: parse error in message type %d", tag)
}
func findCommon(what string, client []string, server []string) (common string, err error) {
for _, c := range client {
for _, s := range server {
if c == s {
return c, nil
}
}
}
return "", fmt.Errorf("ssh: no common algorithm for %s; client offered: %v, server offered: %v", what, client, server)
}
type directionAlgorithms struct {
Cipher string
MAC string
Compression string
}
type algorithms struct {
kex string
hostKey string
w directionAlgorithms
r directionAlgorithms
}
func findAgreedAlgorithms(clientKexInit, serverKexInit *kexInitMsg) (algs *algorithms, err error) {
result := &algorithms{}
result.kex, err = findCommon("key exchange", clientKexInit.KexAlgos, serverKexInit.KexAlgos)
if err != nil {
return
}
result.hostKey, err = findCommon("host key", clientKexInit.ServerHostKeyAlgos, serverKexInit.ServerHostKeyAlgos)
if err != nil {
return
}
result.w.Cipher, err = findCommon("client to server cipher", clientKexInit.CiphersClientServer, serverKexInit.CiphersClientServer)
if err != nil {
return
}
result.r.Cipher, err = findCommon("server to client cipher", clientKexInit.CiphersServerClient, serverKexInit.CiphersServerClient)
if err != nil {
return
}
result.w.MAC, err = findCommon("client to server MAC", clientKexInit.MACsClientServer, serverKexInit.MACsClientServer)
if err != nil {
return
}
result.r.MAC, err = findCommon("server to client MAC", clientKexInit.MACsServerClient, serverKexInit.MACsServerClient)
if err != nil {
return
}
result.w.Compression, err = findCommon("client to server compression", clientKexInit.CompressionClientServer, serverKexInit.CompressionClientServer)
if err != nil {
return
}
result.r.Compression, err = findCommon("server to client compression", clientKexInit.CompressionServerClient, serverKexInit.CompressionServerClient)
if err != nil {
return
}
return result, nil
}
// If rekeythreshold is too small, we can't make any progress sending
// stuff.
const minRekeyThreshold uint64 = 256
// Config contains configuration data common to both ServerConfig and
// ClientConfig.
type Config struct {
// Rand provides the source of entropy for cryptographic
// primitives. If Rand is nil, the cryptographic random reader
// in package crypto/rand will be used.
Rand io.Reader
// The maximum number of bytes sent or received after which a
// new key is negotiated. It must be at least 256. If
// unspecified, 1 gigabyte is used.
RekeyThreshold uint64
// The allowed key exchanges algorithms. If unspecified then a
// default set of algorithms is used.
KeyExchanges []string
// The allowed cipher algorithms. If unspecified then a sensible
// default is used.
Ciphers []string
// The allowed MAC algorithms. If unspecified then a sensible default
// is used.
MACs []string
}
// SetDefaults sets sensible values for unset fields in config. This is
// exported for testing: Configs passed to SSH functions are copied and have
// default values set automatically.
func (c *Config) SetDefaults() {
if c.Rand == nil {
c.Rand = rand.Reader
}
if c.Ciphers == nil {
c.Ciphers = supportedCiphers
}
var ciphers []string
for _, c := range c.Ciphers {
if cipherModes[c] != nil {
// reject the cipher if we have no cipherModes definition
ciphers = append(ciphers, c)
}
}
c.Ciphers = ciphers
if c.KeyExchanges == nil {
c.KeyExchanges = supportedKexAlgos
}
if c.MACs == nil {
c.MACs = supportedMACs
}
if c.RekeyThreshold == 0 {
// RFC 4253, section 9 suggests rekeying after 1G.
c.RekeyThreshold = 1 << 30
}
if c.RekeyThreshold < minRekeyThreshold {
c.RekeyThreshold = minRekeyThreshold
}
}
// buildDataSignedForAuth returns the data that is signed in order to prove
// possession of a private key. See RFC 4252, section 7.
func buildDataSignedForAuth(sessionId []byte, req userAuthRequestMsg, algo, pubKey []byte) []byte {
data := struct {
Session []byte
Type byte
User string
Service string
Method string
Sign bool
Algo []byte
PubKey []byte
}{
sessionId,
msgUserAuthRequest,
req.User,
req.Service,
req.Method,
true,
algo,
pubKey,
}
return Marshal(data)
}
func appendU16(buf []byte, n uint16) []byte {
return append(buf, byte(n>>8), byte(n))
}
func appendU32(buf []byte, n uint32) []byte {
return append(buf, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
}
func appendU64(buf []byte, n uint64) []byte {
return append(buf,
byte(n>>56), byte(n>>48), byte(n>>40), byte(n>>32),
byte(n>>24), byte(n>>16), byte(n>>8), byte(n))
}
func appendInt(buf []byte, n int) []byte {
return appendU32(buf, uint32(n))
}
func appendString(buf []byte, s string) []byte {
buf = appendU32(buf, uint32(len(s)))
buf = append(buf, s...)
return buf
}
func appendBool(buf []byte, b bool) []byte {
if b {
return append(buf, 1)
}
return append(buf, 0)
}
// newCond is a helper to hide the fact that there is no usable zero
// value for sync.Cond.
func newCond() *sync.Cond { return sync.NewCond(new(sync.Mutex)) }
// window represents the buffer available to clients
// wishing to write to a channel.
type window struct {
*sync.Cond
win uint32 // RFC 4254 5.2 says the window size can grow to 2^32-1
writeWaiters int
closed bool
}
// add adds win to the amount of window available
// for consumers.
func (w *window) add(win uint32) bool {
// a zero sized window adjust is a noop.
if win == 0 {
return true
}
w.L.Lock()
if w.win+win < win {
w.L.Unlock()
return false
}
w.win += win
// It is unusual that multiple goroutines would be attempting to reserve
// window space, but not guaranteed. Use broadcast to notify all waiters
// that additional window is available.
w.Broadcast()
w.L.Unlock()
return true
}
// close sets the window to closed, so all reservations fail
// immediately.
func (w *window) close() {
w.L.Lock()
w.closed = true
w.Broadcast()
w.L.Unlock()
}
// reserve reserves win from the available window capacity.
// If no capacity remains, reserve will block. reserve may
// return less than requested.
func (w *window) reserve(win uint32) (uint32, error) {
var err error
w.L.Lock()
w.writeWaiters++
w.Broadcast()
for w.win == 0 && !w.closed {
w.Wait()
}
w.writeWaiters--
if w.win < win {
win = w.win
}
w.win -= win
if w.closed {
err = io.EOF
}
w.L.Unlock()
return win, err
}
// waitWriterBlocked waits until some goroutine is blocked for further
// writes. It is used in tests only.
func (w *window) waitWriterBlocked() {
w.Cond.L.Lock()
for w.writeWaiters == 0 {
w.Cond.Wait()
}
w.Cond.L.Unlock()
}

144
modules/crypto/ssh/connection.go

@ -1,144 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"fmt"
"net"
)
// OpenChannelError is returned if the other side rejects an
// OpenChannel request.
type OpenChannelError struct {
Reason RejectionReason
Message string
}
func (e *OpenChannelError) Error() string {
return fmt.Sprintf("ssh: rejected: %s (%s)", e.Reason, e.Message)
}
// ConnMetadata holds metadata for the connection.
type ConnMetadata interface {
// User returns the user ID for this connection.
// It is empty if no authentication is used.
User() string
// SessionID returns the sesson hash, also denoted by H.
SessionID() []byte
// ClientVersion returns the client's version string as hashed
// into the session ID.
ClientVersion() []byte
// ServerVersion returns the server's version string as hashed
// into the session ID.
ServerVersion() []byte
// RemoteAddr returns the remote address for this connection.
RemoteAddr() net.Addr
// LocalAddr returns the local address for this connection.
LocalAddr() net.Addr
}
// Conn represents an SSH connection for both server and client roles.
// Conn is the basis for implementing an application layer, such
// as ClientConn, which implements the traditional shell access for
// clients.
type Conn interface {
ConnMetadata
// SendRequest sends a global request, and returns the
// reply. If wantReply is true, it returns the response status
// and payload. See also RFC4254, section 4.
SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error)
// OpenChannel tries to open an channel. If the request is
// rejected, it returns *OpenChannelError. On success it returns
// the SSH Channel and a Go channel for incoming, out-of-band
// requests. The Go channel must be serviced, or the
// connection will hang.
OpenChannel(name string, data []byte) (Channel, <-chan *Request, error)
// Close closes the underlying network connection
Close() error
// Wait blocks until the connection has shut down, and returns the
// error causing the shutdown.
Wait() error
// TODO(hanwen): consider exposing:
// RequestKeyChange
// Disconnect
}
// DiscardRequests consumes and rejects all requests from the
// passed-in channel.
func DiscardRequests(in <-chan *Request) {
for req := range in {
if req.WantReply {
req.Reply(false, nil)
}
}
}
// A connection represents an incoming connection.
type connection struct {
transport *handshakeTransport
sshConn
// The connection protocol.
*mux
}
func (c *connection) Close() error {
return c.sshConn.conn.Close()
}
// sshconn provides net.Conn metadata, but disallows direct reads and
// writes.
type sshConn struct {
conn net.Conn
user string
sessionID []byte
clientVersion []byte
serverVersion []byte
}
func dup(src []byte) []byte {
dst := make([]byte, len(src))
copy(dst, src)
return dst
}
func (c *sshConn) User() string {
return c.user
}
func (c *sshConn) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *sshConn) Close() error {
return c.conn.Close()
}
func (c *sshConn) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *sshConn) SessionID() []byte {
return dup(c.sessionID)
}
func (c *sshConn) ClientVersion() []byte {
return dup(c.clientVersion)
}
func (c *sshConn) ServerVersion() []byte {
return dup(c.serverVersion)
}

18
modules/crypto/ssh/doc.go

@ -1,18 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package ssh implements an SSH client and server.
SSH is a transport security protocol, an authentication protocol and a
family of application protocols. The most typical application level
protocol is a remote shell and this is specifically implemented. However,
the multiplexed nature of SSH is exposed to users that wish to support
others.
References:
[PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD
[SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1
*/
package ssh

211
modules/crypto/ssh/example_test.go

@ -1,211 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh_test
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"github.com/gogits/gogs/modules/crypto/ssh"
"github.com/gogits/gogs/modules/crypto/ssh/terminal"
)
func ExampleNewServerConn() {
// An SSH server is represented by a ServerConfig, which holds
// certificate details and handles authentication of ServerConns.
config := &ssh.ServerConfig{
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
// Should use constant-time compare (or better, salt+hash) in
// a production setting.
if c.User() == "testuser" && string(pass) == "tiger" {
return nil, nil
}
return nil, fmt.Errorf("password rejected for %q", c.User())
},
}
privateBytes, err := ioutil.ReadFile("id_rsa")
if err != nil {
panic("Failed to load private key")
}
private, err := ssh.ParsePrivateKey(privateBytes)
if err != nil {
panic("Failed to parse private key")
}
config.AddHostKey(private)
// Once a ServerConfig has been configured, connections can be
// accepted.
listener, err := net.Listen("tcp", "0.0.0.0:2022")
if err != nil {
panic("failed to listen for connection")
}
nConn, err := listener.Accept()
if err != nil {
panic("failed to accept incoming connection")
}
// Before use, a handshake must be performed on the incoming
// net.Conn.
_, chans, reqs, err := ssh.NewServerConn(nConn, config)
if err != nil {
panic("failed to handshake")
}
// The incoming Request channel must be serviced.
go ssh.DiscardRequests(reqs)
// Service the incoming Channel channel.
for newChannel := range chans {
// Channels have a type, depending on the application level
// protocol intended. In the case of a shell, the type is
// "session" and ServerShell may be used to present a simple
// terminal interface.
if newChannel.ChannelType() != "session" {
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
channel, requests, err := newChannel.Accept()
if err != nil {
panic("could not accept channel.")
}
// Sessions have out-of-band requests such as "shell",
// "pty-req" and "env". Here we handle only the
// "shell" request.
go func(in <-chan *ssh.Request) {
for req := range in {
ok := false
switch req.Type {
case "shell":
ok = true
if len(req.Payload) > 0 {
// We don't accept any
// commands, only the
// default shell.
ok = false
}
}
req.Reply(ok, nil)
}
}(requests)
term := terminal.NewTerminal(channel, "> ")
go func() {
defer channel.Close()
for {
line, err := term.ReadLine()
if err != nil {
break
}
fmt.Println(line)
}
}()
}
}
func ExampleDial() {
// An SSH client is represented with a ClientConn. Currently only
// the "password" authentication method is supported.
//
// To authenticate with the remote server you must pass at least one
// implementation of AuthMethod via the Auth field in ClientConfig.
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("yourpassword"),
},
}
client, err := ssh.Dial("tcp", "yourserver.com:22", config)
if err != nil {
panic("Failed to dial: " + err.Error())
}
// Each ClientConn can support multiple interactive sessions,
// represented by a Session.
session, err := client.NewSession()
if err != nil {
panic("Failed to create session: " + err.Error())
}
defer session.Close()
// Once a Session is created, you can execute a single command on
// the remote side using the Run method.
var b bytes.Buffer
session.Stdout = &b
if err := session.Run("/usr/bin/whoami"); err != nil {
panic("Failed to run: " + err.Error())
}
fmt.Println(b.String())
}
func ExampleClient_Listen() {
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
}
// Dial your ssh server.
conn, err := ssh.Dial("tcp", "localhost:22", config)
if err != nil {
log.Fatalf("unable to connect: %s", err)
}
defer conn.Close()
// Request the remote side to open port 8080 on all interfaces.
l, err := conn.Listen("tcp", "0.0.0.0:8080")
if err != nil {
log.Fatalf("unable to register tcp forward: %v", err)
}
defer l.Close()
// Serve HTTP with your SSH server acting as a reverse proxy.
http.Serve(l, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintf(resp, "Hello world!\n")
}))
}
func ExampleSession_RequestPty() {
// Create client config
config := &ssh.ClientConfig{
User: "username",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
}
// Connect to ssh server
conn, err := ssh.Dial("tcp", "localhost:22", config)
if err != nil {
log.Fatalf("unable to connect: %s", err)
}
defer conn.Close()
// Create a session
session, err := conn.NewSession()
if err != nil {
log.Fatalf("unable to create session: %s", err)
}
defer session.Close()
// Set up terminal modes
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
// Request pseudo terminal
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
log.Fatalf("request for pseudo terminal failed: %s", err)
}
// Start remote shell
if err := session.Shell(); err != nil {
log.Fatalf("failed to start shell: %s", err)
}
}

412
modules/crypto/ssh/handshake.go

@ -1,412 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"crypto/rand"
"errors"
"fmt"
"io"
"log"
"net"
"sync"
)
// debugHandshake, if set, prints messages sent and received. Key
// exchange messages are printed as if DH were used, so the debug
// messages are wrong when using ECDH.
const debugHandshake = false
// keyingTransport is a packet based transport that supports key
// changes. It need not be thread-safe. It should pass through
// msgNewKeys in both directions.
type keyingTransport interface {
packetConn
// prepareKeyChange sets up a key change. The key change for a
// direction will be effected if a msgNewKeys message is sent
// or received.
prepareKeyChange(*algorithms, *kexResult) error
// getSessionID returns the session ID. prepareKeyChange must
// have been called once.
getSessionID() []byte
}
// rekeyingTransport is the interface of handshakeTransport that we
// (internally) expose to ClientConn and ServerConn.
type rekeyingTransport interface {
packetConn
// requestKeyChange asks the remote side to change keys. All
// writes are blocked until the key change succeeds, which is
// signaled by reading a msgNewKeys.
requestKeyChange() error
// getSessionID returns the session ID. This is only valid
// after the first key change has completed.
getSessionID() []byte
}
// handshakeTransport implements rekeying on top of a keyingTransport
// and offers a thread-safe writePacket() interface.
type handshakeTransport struct {
conn keyingTransport
config *Config
serverVersion []byte
clientVersion []byte
// hostKeys is non-empty if we are the server. In that case,
// it contains all host keys that can be used to sign the
// connection.
hostKeys []Signer
// hostKeyAlgorithms is non-empty if we are the client. In that case,
// we accept these key types from the server as host key.
hostKeyAlgorithms []string
// On read error, incoming is closed, and readError is set.
incoming chan []byte
readError error
// data for host key checking
hostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error
dialAddress string
remoteAddr net.Addr
readSinceKex uint64
// Protects the writing side of the connection
mu sync.Mutex
cond *sync.Cond
sentInitPacket []byte
sentInitMsg *kexInitMsg
writtenSinceKex uint64
writeError error
}
func newHandshakeTransport(conn keyingTransport, config *Config, clientVersion, serverVersion []byte) *handshakeTransport {
t := &handshakeTransport{
conn: conn,
serverVersion: serverVersion,
clientVersion: clientVersion,
incoming: make(chan []byte, 16),
config: config,
}
t.cond = sync.NewCond(&t.mu)
return t
}
func newClientTransport(conn keyingTransport, clientVersion, serverVersion []byte, config *ClientConfig, dialAddr string, addr net.Addr) *handshakeTransport {
t := newHandshakeTransport(conn, &config.Config, clientVersion, serverVersion)
t.dialAddress = dialAddr
t.remoteAddr = addr
t.hostKeyCallback = config.HostKeyCallback
if config.HostKeyAlgorithms != nil {
t.hostKeyAlgorithms = config.HostKeyAlgorithms
} else {
t.hostKeyAlgorithms = supportedHostKeyAlgos
}
go t.readLoop()
return t
}
func newServerTransport(conn keyingTransport, clientVersion, serverVersion []byte, config *ServerConfig) *handshakeTransport {
t := newHandshakeTransport(conn, &config.Config, clientVersion, serverVersion)
t.hostKeys = config.hostKeys
go t.readLoop()
return t
}
func (t *handshakeTransport) getSessionID() []byte {
return t.conn.getSessionID()
}
func (t *handshakeTransport) id() string {
if len(t.hostKeys) > 0 {
return "server"
}
return "client"
}
func (t *handshakeTransport) readPacket() ([]byte, error) {
p, ok := <-t.incoming
if !ok {
return nil, t.readError
}
return p, nil
}
func (t *handshakeTransport) readLoop() {
for {
p, err := t.readOnePacket()
if err != nil {
t.readError = err
close(t.incoming)
break
}
if p[0] == msgIgnore || p[0] == msgDebug {
continue
}
t.incoming <- p
}
// If we can't read, declare the writing part dead too.
t.mu.Lock()
defer t.mu.Unlock()
if t.writeError == nil {
t.writeError = t.readError
}
t.cond.Broadcast()
}
func (t *handshakeTransport) readOnePacket() ([]byte, error) {
if t.readSinceKex > t.config.RekeyThreshold {
if err := t.requestKeyChange(); err != nil {
return nil, err
}
}
p, err := t.conn.readPacket()
if err != nil {
return nil, err
}
t.readSinceKex += uint64(len(p))
if debugHandshake {
msg, err := decode(p)
log.Printf("%s got %T %v (%v)", t.id(), msg, msg, err)
}
if p[0] != msgKexInit {
return p, nil
}
err = t.enterKeyExchange(p)
t.mu.Lock()
if err != nil {
// drop connection
t.conn.Close()
t.writeError = err
}
if debugHandshake {
log.Printf("%s exited key exchange, err %v", t.id(), err)
}
// Unblock writers.
t.sentInitMsg = nil
t.sentInitPacket = nil
t.cond.Broadcast()
t.writtenSinceKex = 0
t.mu.Unlock()
if err != nil {
return nil, err
}
t.readSinceKex = 0
return []byte{msgNewKeys}, nil
}
// sendKexInit sends a key change message, and returns the message
// that was sent. After initiating the key change, all writes will be
// blocked until the change is done, and a failed key change will
// close the underlying transport. This function is safe for
// concurrent use by multiple goroutines.
func (t *handshakeTransport) sendKexInit() (*kexInitMsg, []byte, error) {
t.mu.Lock()
defer t.mu.Unlock()
return t.sendKexInitLocked()
}
func (t *handshakeTransport) requestKeyChange() error {
_, _, err := t.sendKexInit()
return err
}
// sendKexInitLocked sends a key change message. t.mu must be locked
// while this happens.
func (t *handshakeTransport) sendKexInitLocked() (*kexInitMsg, []byte, error) {
// kexInits may be sent either in response to the other side,
// or because our side wants to initiate a key change, so we
// may have already sent a kexInit. In that case, don't send a
// second kexInit.
if t.sentInitMsg != nil {
return t.sentInitMsg, t.sentInitPacket, nil
}
msg := &kexInitMsg{
KexAlgos: t.config.KeyExchanges,
CiphersClientServer: t.config.Ciphers,
CiphersServerClient: t.config.Ciphers,
MACsClientServer: t.config.MACs,
MACsServerClient: t.config.MACs,
CompressionClientServer: supportedCompressions,
CompressionServerClient: supportedCompressions,
}
io.ReadFull(rand.Reader, msg.Cookie[:])
if len(t.hostKeys) > 0 {
for _, k := range t.hostKeys {
msg.ServerHostKeyAlgos = append(
msg.ServerHostKeyAlgos, k.PublicKey().Type())
}
} else {
msg.ServerHostKeyAlgos = t.hostKeyAlgorithms
}
packet := Marshal(msg)
// writePacket destroys the contents, so save a copy.
packetCopy := make([]byte, len(packet))
copy(packetCopy, packet)
if err := t.conn.writePacket(packetCopy); err != nil {
return nil, nil, err
}
t.sentInitMsg = msg
t.sentInitPacket = packet
return msg, packet, nil
}
func (t *handshakeTransport) writePacket(p []byte) error {
t.mu.Lock()
defer t.mu.Unlock()
if t.writtenSinceKex > t.config.RekeyThreshold {
t.sendKexInitLocked()
}
for t.sentInitMsg != nil && t.writeError == nil {
t.cond.Wait()
}
if t.writeError != nil {
return t.writeError
}
t.writtenSinceKex += uint64(len(p))
switch p[0] {
case msgKexInit:
return errors.New("ssh: only handshakeTransport can send kexInit")
case msgNewKeys:
return errors.New("ssh: only handshakeTransport can send newKeys")
default:
return t.conn.writePacket(p)
}
}
func (t *handshakeTransport) Close() error {
return t.conn.Close()
}
// enterKeyExchange runs the key exchange.
func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error {
if debugHandshake {
log.Printf("%s entered key exchange", t.id())
}
myInit, myInitPacket, err := t.sendKexInit()
if err != nil {
return err
}
otherInit := &kexInitMsg{}
if err := Unmarshal(otherInitPacket, otherInit); err != nil {
return err
}
magics := handshakeMagics{
clientVersion: t.clientVersion,
serverVersion: t.serverVersion,
clientKexInit: otherInitPacket,
serverKexInit: myInitPacket,
}
clientInit := otherInit
serverInit := myInit
if len(t.hostKeys) == 0 {
clientInit = myInit
serverInit = otherInit
magics.clientKexInit = myInitPacket
magics.serverKexInit = otherInitPacket
}
algs, err := findAgreedAlgorithms(clientInit, serverInit)
if err != nil {
return err
}
// We don't send FirstKexFollows, but we handle receiving it.
if otherInit.FirstKexFollows && algs.kex != otherInit.KexAlgos[0] {
// other side sent a kex message for the wrong algorithm,
// which we have to ignore.
if _, err := t.conn.readPacket(); err != nil {
return err
}
}
kex, ok := kexAlgoMap[algs.kex]
if !ok {
return fmt.Errorf("ssh: unexpected key exchange algorithm %v", algs.kex)
}
var result *kexResult
if len(t.hostKeys) > 0 {
result, err = t.server(kex, algs, &magics)
} else {
result, err = t.client(kex, algs, &magics)
}
if err != nil {
return err
}
t.conn.prepareKeyChange(algs, result)
if err = t.conn.writePacket([]byte{msgNewKeys}); err != nil {
return err
}
if packet, err := t.conn.readPacket(); err != nil {
return err
} else if packet[0] != msgNewKeys {
return unexpectedMessageError(msgNewKeys, packet[0])
}
return nil
}
func (t *handshakeTransport) server(kex kexAlgorithm, algs *algorithms, magics *handshakeMagics) (*kexResult, error) {
var hostKey Signer
for _, k := range t.hostKeys {
if algs.hostKey == k.PublicKey().Type() {
hostKey = k
}
}
r, err := kex.Server(t.conn, t.config.Rand, magics, hostKey)
return r, err
}
func (t *handshakeTransport) client(kex kexAlgorithm, algs *algorithms, magics *handshakeMagics) (*kexResult, error) {
result, err := kex.Client(t.conn, t.config.Rand, magics)
if err != nil {
return nil, err
}
hostKey, err := ParsePublicKey(result.HostKey)
if err != nil {
return nil, err
}
if err := verifyHostKeySignature(hostKey, result); err != nil {
return nil, err
}
if t.hostKeyCallback != nil {
err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey)
if err != nil {
return nil, err
}
}
return result, nil
}

415
modules/crypto/ssh/handshake_test.go

@ -1,415 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"net"
"runtime"
"strings"
"sync"
"testing"
)
type testChecker struct {
calls []string
}
func (t *testChecker) Check(dialAddr string, addr net.Addr, key PublicKey) error {
if dialAddr == "bad" {
return fmt.Errorf("dialAddr is bad")
}
if tcpAddr, ok := addr.(*net.TCPAddr); !ok || tcpAddr == nil {
return fmt.Errorf("testChecker: got %T want *net.TCPAddr", addr)
}
t.calls = append(t.calls, fmt.Sprintf("%s %v %s %x", dialAddr, addr, key.Type(), key.Marshal()))
return nil
}
// netPipe is analogous to net.Pipe, but it uses a real net.Conn, and
// therefore is buffered (net.Pipe deadlocks if both sides start with
// a write.)
func netPipe() (net.Conn, net.Conn, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, nil, err
}
defer listener.Close()
c1, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
return nil, nil, err
}
c2, err := listener.Accept()
if err != nil {
c1.Close()
return nil, nil, err
}
return c1, c2, nil
}
func handshakePair(clientConf *ClientConfig, addr string) (client *handshakeTransport, server *handshakeTransport, err error) {
a, b, err := netPipe()
if err != nil {
return nil, nil, err
}
trC := newTransport(a, rand.Reader, true)
trS := newTransport(b, rand.Reader, false)
clientConf.SetDefaults()
v := []byte("version")
client = newClientTransport(trC, v, v, clientConf, addr, a.RemoteAddr())
serverConf := &ServerConfig{}
serverConf.AddHostKey(testSigners["ecdsa"])
serverConf.AddHostKey(testSigners["rsa"])
serverConf.SetDefaults()
server = newServerTransport(trS, v, v, serverConf)
return client, server, nil
}
func TestHandshakeBasic(t *testing.T) {
if runtime.GOOS == "plan9" {
t.Skip("see golang.org/issue/7237")
}
checker := &testChecker{}
trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
go func() {
// Client writes a bunch of stuff, and does a key
// change in the middle. This should not confuse the
// handshake in progress
for i := 0; i < 10; i++ {
p := []byte{msgRequestSuccess, byte(i)}
if err := trC.writePacket(p); err != nil {
t.Fatalf("sendPacket: %v", err)
}
if i == 5 {
// halfway through, we request a key change.
_, _, err := trC.sendKexInit()
if err != nil {
t.Fatalf("sendKexInit: %v", err)
}
}
}
trC.Close()
}()
// Server checks that client messages come in cleanly
i := 0
for {
p, err := trS.readPacket()
if err != nil {
break
}
if p[0] == msgNewKeys {
continue
}
want := []byte{msgRequestSuccess, byte(i)}
if bytes.Compare(p, want) != 0 {
t.Errorf("message %d: got %q, want %q", i, p, want)
}
i++
}
if i != 10 {
t.Errorf("received %d messages, want 10.", i)
}
// If all went well, we registered exactly 1 key change.
if len(checker.calls) != 1 {
t.Fatalf("got %d host key checks, want 1", len(checker.calls))
}
pub := testSigners["ecdsa"].PublicKey()
want := fmt.Sprintf("%s %v %s %x", "addr", trC.remoteAddr, pub.Type(), pub.Marshal())
if want != checker.calls[0] {
t.Errorf("got %q want %q for host key check", checker.calls[0], want)
}
}
func TestHandshakeError(t *testing.T) {
checker := &testChecker{}
trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "bad")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
// send a packet
packet := []byte{msgRequestSuccess, 42}
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
// Now request a key change.
_, _, err = trC.sendKexInit()
if err != nil {
t.Errorf("sendKexInit: %v", err)
}
// the key change will fail, and afterwards we can't write.
if err := trC.writePacket([]byte{msgRequestSuccess, 43}); err == nil {
t.Errorf("writePacket after botched rekey succeeded.")
}
readback, err := trS.readPacket()
if err != nil {
t.Fatalf("server closed too soon: %v", err)
}
if bytes.Compare(readback, packet) != 0 {
t.Errorf("got %q want %q", readback, packet)
}
readback, err = trS.readPacket()
if err == nil {
t.Errorf("got a message %q after failed key change", readback)
}
}
func TestHandshakeTwice(t *testing.T) {
checker := &testChecker{}
trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
// send a packet
packet := make([]byte, 5)
packet[0] = msgRequestSuccess
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
// Now request a key change.
_, _, err = trC.sendKexInit()
if err != nil {
t.Errorf("sendKexInit: %v", err)
}
// Send another packet. Use a fresh one, since writePacket destroys.
packet = make([]byte, 5)
packet[0] = msgRequestSuccess
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
// 2nd key change.
_, _, err = trC.sendKexInit()
if err != nil {
t.Errorf("sendKexInit: %v", err)
}
packet = make([]byte, 5)
packet[0] = msgRequestSuccess
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
packet = make([]byte, 5)
packet[0] = msgRequestSuccess
for i := 0; i < 5; i++ {
msg, err := trS.readPacket()
if err != nil {
t.Fatalf("server closed too soon: %v", err)
}
if msg[0] == msgNewKeys {
continue
}
if bytes.Compare(msg, packet) != 0 {
t.Errorf("packet %d: got %q want %q", i, msg, packet)
}
}
if len(checker.calls) != 2 {
t.Errorf("got %d key changes, want 2", len(checker.calls))
}
}
func TestHandshakeAutoRekeyWrite(t *testing.T) {
checker := &testChecker{}
clientConf := &ClientConfig{HostKeyCallback: checker.Check}
clientConf.RekeyThreshold = 500
trC, trS, err := handshakePair(clientConf, "addr")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
for i := 0; i < 5; i++ {
packet := make([]byte, 251)
packet[0] = msgRequestSuccess
if err := trC.writePacket(packet); err != nil {
t.Errorf("writePacket: %v", err)
}
}
j := 0
for ; j < 5; j++ {
_, err := trS.readPacket()
if err != nil {
break
}
}
if j != 5 {
t.Errorf("got %d, want 5 messages", j)
}
if len(checker.calls) != 2 {
t.Errorf("got %d key changes, wanted 2", len(checker.calls))
}
}
type syncChecker struct {
called chan int
}
func (t *syncChecker) Check(dialAddr string, addr net.Addr, key PublicKey) error {
t.called <- 1
return nil
}
func TestHandshakeAutoRekeyRead(t *testing.T) {
sync := &syncChecker{make(chan int, 2)}
clientConf := &ClientConfig{
HostKeyCallback: sync.Check,
}
clientConf.RekeyThreshold = 500
trC, trS, err := handshakePair(clientConf, "addr")
if err != nil {
t.Fatalf("handshakePair: %v", err)
}
defer trC.Close()
defer trS.Close()
packet := make([]byte, 501)
packet[0] = msgRequestSuccess
if err := trS.writePacket(packet); err != nil {
t.Fatalf("writePacket: %v", err)
}
// While we read out the packet, a key change will be
// initiated.
if _, err := trC.readPacket(); err != nil {
t.Fatalf("readPacket(client): %v", err)
}
<-sync.called
}
// errorKeyingTransport generates errors after a given number of
// read/write operations.
type errorKeyingTransport struct {
packetConn
readLeft, writeLeft int
}
func (n *errorKeyingTransport) prepareKeyChange(*algorithms, *kexResult) error {
return nil
}
func (n *errorKeyingTransport) getSessionID() []byte {
return nil
}
func (n *errorKeyingTransport) writePacket(packet []byte) error {
if n.writeLeft == 0 {
n.Close()
return errors.New("barf")
}
n.writeLeft--
return n.packetConn.writePacket(packet)
}
func (n *errorKeyingTransport) readPacket() ([]byte, error) {
if n.readLeft == 0 {
n.Close()
return nil, errors.New("barf")
}
n.readLeft--
return n.packetConn.readPacket()
}
func TestHandshakeErrorHandlingRead(t *testing.T) {
for i := 0; i < 20; i++ {
testHandshakeErrorHandlingN(t, i, -1)
}
}
func TestHandshakeErrorHandlingWrite(t *testing.T) {
for i := 0; i < 20; i++ {
testHandshakeErrorHandlingN(t, -1, i)
}
}
// testHandshakeErrorHandlingN runs handshakes, injecting errors. If
// handshakeTransport deadlocks, the go runtime will detect it and
// panic.
func testHandshakeErrorHandlingN(t *testing.T, readLimit, writeLimit int) {
msg := Marshal(&serviceRequestMsg{strings.Repeat("x", int(minRekeyThreshold)/4)})
a, b := memPipe()
defer a.Close()
defer b.Close()
key := testSigners["ecdsa"]
serverConf := Config{RekeyThreshold: minRekeyThreshold}
serverConf.SetDefaults()
serverConn := newHandshakeTransport(&errorKeyingTransport{a, readLimit, writeLimit}, &serverConf, []byte{'a'}, []byte{'b'})
serverConn.hostKeys = []Signer{key}
go serverConn.readLoop()
clientConf := Config{RekeyThreshold: 10 * minRekeyThreshold}
clientConf.SetDefaults()
clientConn := newHandshakeTransport(&errorKeyingTransport{b, -1, -1}, &clientConf, []byte{'a'}, []byte{'b'})
clientConn.hostKeyAlgorithms = []string{key.PublicKey().Type()}
go clientConn.readLoop()
var wg sync.WaitGroup
wg.Add(4)
for _, hs := range []packetConn{serverConn, clientConn} {
go func(c packetConn) {
for {
err := c.writePacket(msg)
if err != nil {
break
}
}
wg.Done()
}(hs)
go func(c packetConn) {
for {
_, err := c.readPacket()
if err != nil {
break
}
}
wg.Done()
}(hs)
}
wg.Wait()
}

526
modules/crypto/ssh/kex.go

@ -1,526 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/subtle"
"crypto/rand"
"errors"
"io"
"math/big"
"golang.org/x/crypto/curve25519"
)
const (
kexAlgoDH1SHA1 = "diffie-hellman-group1-sha1"
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
kexAlgoECDH256 = "ecdh-sha2-nistp256"
kexAlgoECDH384 = "ecdh-sha2-nistp384"
kexAlgoECDH521 = "ecdh-sha2-nistp521"
kexAlgoCurve25519SHA256 = "curve25519-sha256@libssh.org"
)
// kexResult captures the outcome of a key exchange.
type kexResult struct {
// Session hash. See also RFC 4253, section 8.
H []byte
// Shared secret. See also RFC 4253, section 8.
K []byte
// Host key as hashed into H.
HostKey []byte
// Signature of H.
Signature []byte
// A cryptographic hash function that matches the security
// level of the key exchange algorithm. It is used for
// calculating H, and for deriving keys from H and K.
Hash crypto.Hash
// The session ID, which is the first H computed. This is used
// to signal data inside transport.
SessionID []byte
}
// handshakeMagics contains data that is always included in the
// session hash.
type handshakeMagics struct {
clientVersion, serverVersion []byte
clientKexInit, serverKexInit []byte
}
func (m *handshakeMagics) write(w io.Writer) {
writeString(w, m.clientVersion)
writeString(w, m.serverVersion)
writeString(w, m.clientKexInit)
writeString(w, m.serverKexInit)
}
// kexAlgorithm abstracts different key exchange algorithms.
type kexAlgorithm interface {
// Server runs server-side key agreement, signing the result
// with a hostkey.
Server(p packetConn, rand io.Reader, magics *handshakeMagics, s Signer) (*kexResult, error)
// Client runs the client-side key agreement. Caller is
// responsible for verifying the host key signature.
Client(p packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error)
}
// dhGroup is a multiplicative group suitable for implementing Diffie-Hellman key agreement.
type dhGroup struct {
g, p *big.Int
}
func (group *dhGroup) diffieHellman(theirPublic, myPrivate *big.Int) (*big.Int, error) {
if theirPublic.Sign() <= 0 || theirPublic.Cmp(group.p) >= 0 {
return nil, errors.New("ssh: DH parameter out of bounds")
}
return new(big.Int).Exp(theirPublic, myPrivate, group.p), nil
}
func (group *dhGroup) Client(c packetConn, randSource io.Reader, magics *handshakeMagics) (*kexResult, error) {
hashFunc := crypto.SHA1
x, err := rand.Int(randSource, group.p)
if err != nil {
return nil, err
}
X := new(big.Int).Exp(group.g, x, group.p)
kexDHInit := kexDHInitMsg{
X: X,
}
if err := c.writePacket(Marshal(&kexDHInit)); err != nil {
return nil, err
}
packet, err := c.readPacket()
if err != nil {
return nil, err
}
var kexDHReply kexDHReplyMsg
if err = Unmarshal(packet, &kexDHReply); err != nil {
return nil, err
}
kInt, err := group.diffieHellman(kexDHReply.Y, x)
if err != nil {
return nil, err
}
h := hashFunc.New()
magics.write(h)
writeString(h, kexDHReply.HostKey)
writeInt(h, X)
writeInt(h, kexDHReply.Y)
K := make([]byte, intLength(kInt))
marshalInt(K, kInt)
h.Write(K)
return &kexResult{
H: h.Sum(nil),
K: K,
HostKey: kexDHReply.HostKey,
Signature: kexDHReply.Signature,
Hash: crypto.SHA1,
}, nil
}
func (group *dhGroup) Server(c packetConn, randSource io.Reader, magics *handshakeMagics, priv Signer) (result *kexResult, err error) {
hashFunc := crypto.SHA1
packet, err := c.readPacket()
if err != nil {
return
}
var kexDHInit kexDHInitMsg
if err = Unmarshal(packet, &kexDHInit); err != nil {
return
}
y, err := rand.Int(randSource, group.p)
if err != nil {
return
}
Y := new(big.Int).Exp(group.g, y, group.p)
kInt, err := group.diffieHellman(kexDHInit.X, y)
if err != nil {
return nil, err
}
hostKeyBytes := priv.PublicKey().Marshal()
h := hashFunc.New()
magics.write(h)
writeString(h, hostKeyBytes)
writeInt(h, kexDHInit.X)
writeInt(h, Y)
K := make([]byte, intLength(kInt))
marshalInt(K, kInt)
h.Write(K)
H := h.Sum(nil)
// H is already a hash, but the hostkey signing will apply its
// own key-specific hash algorithm.
sig, err := signAndMarshal(priv, randSource, H)
if err != nil {
return nil, err
}
kexDHReply := kexDHReplyMsg{
HostKey: hostKeyBytes,
Y: Y,
Signature: sig,
}
packet = Marshal(&kexDHReply)
err = c.writePacket(packet)
return &kexResult{
H: H,
K: K,
HostKey: hostKeyBytes,
Signature: sig,
Hash: crypto.SHA1,
}, nil
}
// ecdh performs Elliptic Curve Diffie-Hellman key exchange as
// described in RFC 5656, section 4.
type ecdh struct {
curve elliptic.Curve
}
func (kex *ecdh) Client(c packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error) {
ephKey, err := ecdsa.GenerateKey(kex.curve, rand)
if err != nil {
return nil, err
}
kexInit := kexECDHInitMsg{
ClientPubKey: elliptic.Marshal(kex.curve, ephKey.PublicKey.X, ephKey.PublicKey.Y),
}
serialized := Marshal(&kexInit)
if err := c.writePacket(serialized); err != nil {
return nil, err
}
packet, err := c.readPacket()
if err != nil {
return nil, err
}
var reply kexECDHReplyMsg
if err = Unmarshal(packet, &reply); err != nil {
return nil, err
}
x, y, err := unmarshalECKey(kex.curve, reply.EphemeralPubKey)
if err != nil {
return nil, err
}
// generate shared secret
secret, _ := kex.curve.ScalarMult(x, y, ephKey.D.Bytes())
h := ecHash(kex.curve).New()
magics.write(h)
writeString(h, reply.HostKey)
writeString(h, kexInit.ClientPubKey)
writeString(h, reply.EphemeralPubKey)
K := make([]byte, intLength(secret))
marshalInt(K, secret)
h.Write(K)
return &kexResult{
H: h.Sum(nil),
K: K,
HostKey: reply.HostKey,
Signature: reply.Signature,
Hash: ecHash(kex.curve),
}, nil
}
// unmarshalECKey parses and checks an EC key.
func unmarshalECKey(curve elliptic.Curve, pubkey []byte) (x, y *big.Int, err error) {
x, y = elliptic.Unmarshal(curve, pubkey)
if x == nil {
return nil, nil, errors.New("ssh: elliptic.Unmarshal failure")
}
if !validateECPublicKey(curve, x, y) {
return nil, nil, errors.New("ssh: public key not on curve")
}
return x, y, nil
}
// validateECPublicKey checks that the point is a valid public key for
// the given curve. See [SEC1], 3.2.2
func validateECPublicKey(curve elliptic.Curve, x, y *big.Int) bool {
if x.Sign() == 0 && y.Sign() == 0 {
return false
}
if x.Cmp(curve.Params().P) >= 0 {
return false
}
if y.Cmp(curve.Params().P) >= 0 {
return false
}
if !curve.IsOnCurve(x, y) {
return false
}
// We don't check if N * PubKey == 0, since
//
// - the NIST curves have cofactor = 1, so this is implicit.
// (We don't foresee an implementation that supports non NIST
// curves)
//
// - for ephemeral keys, we don't need to worry about small
// subgroup attacks.
return true
}
func (kex *ecdh) Server(c packetConn, rand io.Reader, magics *handshakeMagics, priv Signer) (result *kexResult, err error) {
packet, err := c.readPacket()
if err != nil {
return nil, err
}
var kexECDHInit kexECDHInitMsg
if err = Unmarshal(packet, &kexECDHInit); err != nil {
return nil, err
}
clientX, clientY, err := unmarshalECKey(kex.curve, kexECDHInit.ClientPubKey)
if err != nil {
return nil, err
}
// We could cache this key across multiple users/multiple
// connection attempts, but the benefit is small. OpenSSH
// generates a new key for each incoming connection.
ephKey, err := ecdsa.GenerateKey(kex.curve, rand)
if err != nil {
return nil, err
}
hostKeyBytes := priv.PublicKey().Marshal()
serializedEphKey := elliptic.Marshal(kex.curve, ephKey.PublicKey.X, ephKey.PublicKey.Y)
// generate shared secret
secret, _ := kex.curve.ScalarMult(clientX, clientY, ephKey.D.Bytes())
h := ecHash(kex.curve).New()
magics.write(h)
writeString(h, hostKeyBytes)
writeString(h, kexECDHInit.ClientPubKey)
writeString(h, serializedEphKey)
K := make([]byte, intLength(secret))
marshalInt(K, secret)
h.Write(K)
H := h.Sum(nil)
// H is already a hash, but the hostkey signing will apply its
// own key-specific hash algorithm.
sig, err := signAndMarshal(priv, rand, H)
if err != nil {
return nil, err
}
reply := kexECDHReplyMsg{
EphemeralPubKey: serializedEphKey,
HostKey: hostKeyBytes,
Signature: sig,
}
serialized := Marshal(&reply)
if err := c.writePacket(serialized); err != nil {
return nil, err
}
return &kexResult{
H: H,
K: K,
HostKey: reply.HostKey,
Signature: sig,
Hash: ecHash(kex.curve),
}, nil
}
var kexAlgoMap = map[string]kexAlgorithm{}
func init() {
// This is the group called diffie-hellman-group1-sha1 in RFC
// 4253 and Oakley Group 2 in RFC 2409.
p, _ := new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF", 16)
kexAlgoMap[kexAlgoDH1SHA1] = &dhGroup{
g: new(big.Int).SetInt64(2),
p: p,
}
// This is the group called diffie-hellman-group14-sha1 in RFC
// 4253 and Oakley Group 14 in RFC 3526.
p, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", 16)
kexAlgoMap[kexAlgoDH14SHA1] = &dhGroup{
g: new(big.Int).SetInt64(2),
p: p,
}
kexAlgoMap[kexAlgoECDH521] = &ecdh{elliptic.P521()}
kexAlgoMap[kexAlgoECDH384] = &ecdh{elliptic.P384()}
kexAlgoMap[kexAlgoECDH256] = &ecdh{elliptic.P256()}
kexAlgoMap[kexAlgoCurve25519SHA256] = &curve25519sha256{}
}
// curve25519sha256 implements the curve25519-sha256@libssh.org key
// agreement protocol, as described in
// https://git.libssh.org/projects/libssh.git/tree/doc/curve25519-sha256@libssh.org.txt
type curve25519sha256 struct{}
type curve25519KeyPair struct {
priv [32]byte
pub [32]byte
}
func (kp *curve25519KeyPair) generate(rand io.Reader) error {
if _, err := io.ReadFull(rand, kp.priv[:]); err != nil {
return err
}
curve25519.ScalarBaseMult(&kp.pub, &kp.priv)
return nil
}
// curve25519Zeros is just an array of 32 zero bytes so that we have something
// convenient to compare against in order to reject curve25519 points with the
// wrong order.
var curve25519Zeros [32]byte
func (kex *curve25519sha256) Client(c packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error) {
var kp curve25519KeyPair
if err := kp.generate(rand); err != nil {
return nil, err
}
if err := c.writePacket(Marshal(&kexECDHInitMsg{kp.pub[:]})); err != nil {
return nil, err
}
packet, err := c.readPacket()
if err != nil {
return nil, err
}
var reply kexECDHReplyMsg
if err = Unmarshal(packet, &reply); err != nil {
return nil, err
}
if len(reply.EphemeralPubKey) != 32 {
return nil, errors.New("ssh: peer's curve25519 public value has wrong length")
}
var servPub, secret [32]byte
copy(servPub[:], reply.EphemeralPubKey)
curve25519.ScalarMult(&secret, &kp.priv, &servPub)
if subtle.ConstantTimeCompare(secret[:], curve25519Zeros[:]) == 1 {
return nil, errors.New("ssh: peer's curve25519 public value has wrong order")
}
h := crypto.SHA256.New()
magics.write(h)
writeString(h, reply.HostKey)
writeString(h, kp.pub[:])
writeString(h, reply.EphemeralPubKey)
kInt := new(big.Int).SetBytes(secret[:])
K := make([]byte, intLength(kInt))
marshalInt(K, kInt)
h.Write(K)
return &kexResult{
H: h.Sum(nil),
K: K,
HostKey: reply.HostKey,
Signature: reply.Signature,
Hash: crypto.SHA256,
}, nil
}
func (kex *curve25519sha256) Server(c packetConn, rand io.Reader, magics *handshakeMagics, priv Signer) (result *kexResult, err error) {
packet, err := c.readPacket()
if err != nil {
return
}
var kexInit kexECDHInitMsg
if err = Unmarshal(packet, &kexInit); err != nil {
return
}
if len(kexInit.ClientPubKey) != 32 {
return nil, errors.New("ssh: peer's curve25519 public value has wrong length")
}
var kp curve25519KeyPair
if err := kp.generate(rand); err != nil {
return nil, err
}
var clientPub, secret [32]byte
copy(clientPub[:], kexInit.ClientPubKey)
curve25519.ScalarMult(&secret, &kp.priv, &clientPub)
if subtle.ConstantTimeCompare(secret[:], curve25519Zeros[:]) == 1 {
return nil, errors.New("ssh: peer's curve25519 public value has wrong order")
}
hostKeyBytes := priv.PublicKey().Marshal()
h := crypto.SHA256.New()
magics.write(h)
writeString(h, hostKeyBytes)
writeString(h, kexInit.ClientPubKey)
writeString(h, kp.pub[:])
kInt := new(big.Int).SetBytes(secret[:])
K := make([]byte, intLength(kInt))
marshalInt(K, kInt)
h.Write(K)
H := h.Sum(nil)
sig, err := signAndMarshal(priv, rand, H)
if err != nil {
return nil, err
}
reply := kexECDHReplyMsg{
EphemeralPubKey: kp.pub[:],
HostKey: hostKeyBytes,
Signature: sig,
}
if err := c.writePacket(Marshal(&reply)); err != nil {
return nil, err
}
return &kexResult{
H: H,
K: K,
HostKey: hostKeyBytes,
Signature: sig,
Hash: crypto.SHA256,
}, nil
}

50
modules/crypto/ssh/kex_test.go

@ -1,50 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
// Key exchange tests.
import (
"crypto/rand"
"reflect"
"testing"
)
func TestKexes(t *testing.T) {
type kexResultErr struct {
result *kexResult
err error
}
for name, kex := range kexAlgoMap {
a, b := memPipe()
s := make(chan kexResultErr, 1)
c := make(chan kexResultErr, 1)
var magics handshakeMagics
go func() {
r, e := kex.Client(a, rand.Reader, &magics)
a.Close()
c <- kexResultErr{r, e}
}()
go func() {
r, e := kex.Server(b, rand.Reader, &magics, testSigners["ecdsa"])
b.Close()
s <- kexResultErr{r, e}
}()
clientRes := <-c
serverRes := <-s
if clientRes.err != nil {
t.Errorf("client: %v", clientRes.err)
}
if serverRes.err != nil {
t.Errorf("server: %v", serverRes.err)
}
if !reflect.DeepEqual(clientRes.result, serverRes.result) {
t.Errorf("kex %q: mismatch %#v, %#v", name, clientRes.result, serverRes.result)
}
}
}

628
modules/crypto/ssh/keys.go

@ -1,628 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
)
// These constants represent the algorithm names for key types supported by this
// package.
const (
KeyAlgoRSA = "ssh-rsa"
KeyAlgoDSA = "ssh-dss"
KeyAlgoECDSA256 = "ecdsa-sha2-nistp256"
KeyAlgoECDSA384 = "ecdsa-sha2-nistp384"
KeyAlgoECDSA521 = "ecdsa-sha2-nistp521"
)
// parsePubKey parses a public key of the given algorithm.
// Use ParsePublicKey for keys with prepended algorithm.
func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err error) {
switch algo {
case KeyAlgoRSA:
return parseRSA(in)
case KeyAlgoDSA:
return parseDSA(in)
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
return parseECDSA(in)
case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01:
cert, err := parseCert(in, certToPrivAlgo(algo))
if err != nil {
return nil, nil, err
}
return cert, nil, nil
}
return nil, nil, fmt.Errorf("ssh: unknown key algorithm: %v", err)
}
// parseAuthorizedKey parses a public key in OpenSSH authorized_keys format
// (see sshd(8) manual page) once the options and key type fields have been
// removed.
func parseAuthorizedKey(in []byte) (out PublicKey, comment string, err error) {
in = bytes.TrimSpace(in)
i := bytes.IndexAny(in, " \t")
if i == -1 {
i = len(in)
}
base64Key := in[:i]
key := make([]byte, base64.StdEncoding.DecodedLen(len(base64Key)))
n, err := base64.StdEncoding.Decode(key, base64Key)
if err != nil {
return nil, "", err
}
key = key[:n]
out, err = ParsePublicKey(key)
if err != nil {
return nil, "", err
}
comment = string(bytes.TrimSpace(in[i:]))
return out, comment, nil
}
// ParseAuthorizedKeys parses a public key from an authorized_keys
// file used in OpenSSH according to the sshd(8) manual page.
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
for len(in) > 0 {
end := bytes.IndexByte(in, '\n')
if end != -1 {
rest = in[end+1:]
in = in[:end]
} else {
rest = nil
}
end = bytes.IndexByte(in, '\r')
if end != -1 {
in = in[:end]
}
in = bytes.TrimSpace(in)
if len(in) == 0 || in[0] == '#' {
in = rest
continue
}
i := bytes.IndexAny(in, " \t")
if i == -1 {
in = rest
continue
}
if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
return out, comment, options, rest, nil
}
// No key type recognised. Maybe there's an options field at
// the beginning.
var b byte
inQuote := false
var candidateOptions []string
optionStart := 0
for i, b = range in {
isEnd := !inQuote && (b == ' ' || b == '\t')
if (b == ',' && !inQuote) || isEnd {
if i-optionStart > 0 {
candidateOptions = append(candidateOptions, string(in[optionStart:i]))
}
optionStart = i + 1
}
if isEnd {
break
}
if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) {
inQuote = !inQuote
}
}
for i < len(in) && (in[i] == ' ' || in[i] == '\t') {
i++
}
if i == len(in) {
// Invalid line: unmatched quote
in = rest
continue
}
in = in[i:]
i = bytes.IndexAny(in, " \t")
if i == -1 {
in = rest
continue
}
if out, comment, err = parseAuthorizedKey(in[i:]); err == nil {
options = candidateOptions
return out, comment, options, rest, nil
}
in = rest
continue
}
return nil, "", nil, nil, errors.New("ssh: no key found")
}
// ParsePublicKey parses an SSH public key formatted for use in
// the SSH wire protocol according to RFC 4253, section 6.6.
func ParsePublicKey(in []byte) (out PublicKey, err error) {
algo, in, ok := parseString(in)
if !ok {
return nil, errShortRead
}
var rest []byte
out, rest, err = parsePubKey(in, string(algo))
if len(rest) > 0 {
return nil, errors.New("ssh: trailing junk in public key")
}
return out, err
}
// MarshalAuthorizedKey serializes key for inclusion in an OpenSSH
// authorized_keys file. The return value ends with newline.
func MarshalAuthorizedKey(key PublicKey) []byte {
b := &bytes.Buffer{}
b.WriteString(key.Type())
b.WriteByte(' ')
e := base64.NewEncoder(base64.StdEncoding, b)
e.Write(key.Marshal())
e.Close()
b.WriteByte('\n')
return b.Bytes()
}
// PublicKey is an abstraction of different types of public keys.
type PublicKey interface {
// Type returns the key's type, e.g. "ssh-rsa".
Type() string
// Marshal returns the serialized key data in SSH wire format,
// with the name prefix.
Marshal() []byte
// Verify that sig is a signature on the given data using this
// key. This function will hash the data appropriately first.
Verify(data []byte, sig *Signature) error
}
// A Signer can create signatures that verify against a public key.
type Signer interface {
// PublicKey returns an associated PublicKey instance.
PublicKey() PublicKey
// Sign returns raw signature for the given data. This method
// will apply the hash specified for the keytype to the data.
Sign(rand io.Reader, data []byte) (*Signature, error)
}
type rsaPublicKey rsa.PublicKey
func (r *rsaPublicKey) Type() string {
return "ssh-rsa"
}
// parseRSA parses an RSA key according to RFC 4253, section 6.6.
func parseRSA(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
E *big.Int
N *big.Int
Rest []byte `ssh:"rest"`
}
if err := Unmarshal(in, &w); err != nil {
return nil, nil, err
}
if w.E.BitLen() > 24 {
return nil, nil, errors.New("ssh: exponent too large")
}
e := w.E.Int64()
if e < 3 || e&1 == 0 {
return nil, nil, errors.New("ssh: incorrect exponent")
}
var key rsa.PublicKey
key.E = int(e)
key.N = w.N
return (*rsaPublicKey)(&key), w.Rest, nil
}
func (r *rsaPublicKey) Marshal() []byte {
e := new(big.Int).SetInt64(int64(r.E))
wirekey := struct {
Name string
E *big.Int
N *big.Int
}{
KeyAlgoRSA,
e,
r.N,
}
return Marshal(&wirekey)
}
func (r *rsaPublicKey) Verify(data []byte, sig *Signature) error {
if sig.Format != r.Type() {
return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, r.Type())
}
h := crypto.SHA1.New()
h.Write(data)
digest := h.Sum(nil)
return rsa.VerifyPKCS1v15((*rsa.PublicKey)(r), crypto.SHA1, digest, sig.Blob)
}
type rsaPrivateKey struct {
*rsa.PrivateKey
}
func (r *rsaPrivateKey) PublicKey() PublicKey {
return (*rsaPublicKey)(&r.PrivateKey.PublicKey)
}
func (r *rsaPrivateKey) Sign(rand io.Reader, data []byte) (*Signature, error) {
h := crypto.SHA1.New()
h.Write(data)
digest := h.Sum(nil)
blob, err := rsa.SignPKCS1v15(rand, r.PrivateKey, crypto.SHA1, digest)
if err != nil {
return nil, err
}
return &Signature{
Format: r.PublicKey().Type(),
Blob: blob,
}, nil
}
type dsaPublicKey dsa.PublicKey
func (r *dsaPublicKey) Type() string {
return "ssh-dss"
}
// parseDSA parses an DSA key according to RFC 4253, section 6.6.
func parseDSA(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
P, Q, G, Y *big.Int
Rest []byte `ssh:"rest"`
}
if err := Unmarshal(in, &w); err != nil {
return nil, nil, err
}
key := &dsaPublicKey{
Parameters: dsa.Parameters{
P: w.P,
Q: w.Q,
G: w.G,
},
Y: w.Y,
}
return key, w.Rest, nil
}
func (k *dsaPublicKey) Marshal() []byte {
w := struct {
Name string
P, Q, G, Y *big.Int
}{
k.Type(),
k.P,
k.Q,
k.G,
k.Y,
}
return Marshal(&w)
}
func (k *dsaPublicKey) Verify(data []byte, sig *Signature) error {
if sig.Format != k.Type() {
return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, k.Type())
}
h := crypto.SHA1.New()
h.Write(data)
digest := h.Sum(nil)
// Per RFC 4253, section 6.6,
// The value for 'dss_signature_blob' is encoded as a string containing
// r, followed by s (which are 160-bit integers, without lengths or
// padding, unsigned, and in network byte order).
// For DSS purposes, sig.Blob should be exactly 40 bytes in length.
if len(sig.Blob) != 40 {
return errors.New("ssh: DSA signature parse error")
}
r := new(big.Int).SetBytes(sig.Blob[:20])
s := new(big.Int).SetBytes(sig.Blob[20:])
if dsa.Verify((*dsa.PublicKey)(k), digest, r, s) {
return nil
}
return errors.New("ssh: signature did not verify")
}
type dsaPrivateKey struct {
*dsa.PrivateKey
}
func (k *dsaPrivateKey) PublicKey() PublicKey {
return (*dsaPublicKey)(&k.PrivateKey.PublicKey)
}
func (k *dsaPrivateKey) Sign(rand io.Reader, data []byte) (*Signature, error) {
h := crypto.SHA1.New()
h.Write(data)
digest := h.Sum(nil)
r, s, err := dsa.Sign(rand, k.PrivateKey, digest)
if err != nil {
return nil, err
}
sig := make([]byte, 40)
rb := r.Bytes()
sb := s.Bytes()
copy(sig[20-len(rb):20], rb)
copy(sig[40-len(sb):], sb)
return &Signature{
Format: k.PublicKey().Type(),
Blob: sig,
}, nil
}
type ecdsaPublicKey ecdsa.PublicKey
func (key *ecdsaPublicKey) Type() string {
return "ecdsa-sha2-" + key.nistID()
}
func (key *ecdsaPublicKey) nistID() string {
switch key.Params().BitSize {
case 256:
return "nistp256"
case 384:
return "nistp384"
case 521:
return "nistp521"
}
panic("ssh: unsupported ecdsa key size")
}
func supportedEllipticCurve(curve elliptic.Curve) bool {
return curve == elliptic.P256() || curve == elliptic.P384() || curve == elliptic.P521()
}
// ecHash returns the hash to match the given elliptic curve, see RFC
// 5656, section 6.2.1
func ecHash(curve elliptic.Curve) crypto.Hash {
bitSize := curve.Params().BitSize
switch {
case bitSize <= 256:
return crypto.SHA256
case bitSize <= 384:
return crypto.SHA384
}
return crypto.SHA512
}
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
func parseECDSA(in []byte) (out PublicKey, rest []byte, err error) {
var w struct {
Curve string
KeyBytes []byte
Rest []byte `ssh:"rest"`
}
if err := Unmarshal(in, &w); err != nil {
return nil, nil, err
}
key := new(ecdsa.PublicKey)
switch w.Curve {
case "nistp256":
key.Curve = elliptic.P256()
case "nistp384":
key.Curve = elliptic.P384()
case "nistp521":
key.Curve = elliptic.P521()
default:
return nil, nil, errors.New("ssh: unsupported curve")
}
key.X, key.Y = elliptic.Unmarshal(key.Curve, w.KeyBytes)
if key.X == nil || key.Y == nil {
return nil, nil, errors.New("ssh: invalid curve point")
}
return (*ecdsaPublicKey)(key), w.Rest, nil
}
func (key *ecdsaPublicKey) Marshal() []byte {
// See RFC 5656, section 3.1.
keyBytes := elliptic.Marshal(key.Curve, key.X, key.Y)
w := struct {
Name string
ID string
Key []byte
}{
key.Type(),
key.nistID(),
keyBytes,
}
return Marshal(&w)
}
func (key *ecdsaPublicKey) Verify(data []byte, sig *Signature) error {
if sig.Format != key.Type() {
return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, key.Type())
}
h := ecHash(key.Curve).New()
h.Write(data)
digest := h.Sum(nil)
// Per RFC 5656, section 3.1.2,
// The ecdsa_signature_blob value has the following specific encoding:
// mpint r
// mpint s
var ecSig struct {
R *big.Int
S *big.Int
}
if err := Unmarshal(sig.Blob, &ecSig); err != nil {
return err
}
if ecdsa.Verify((*ecdsa.PublicKey)(key), digest, ecSig.R, ecSig.S) {
return nil
}
return errors.New("ssh: signature did not verify")
}
type ecdsaPrivateKey struct {
*ecdsa.PrivateKey
}
func (k *ecdsaPrivateKey) PublicKey() PublicKey {
return (*ecdsaPublicKey)(&k.PrivateKey.PublicKey)
}
func (k *ecdsaPrivateKey) Sign(rand io.Reader, data []byte) (*Signature, error) {
h := ecHash(k.PrivateKey.PublicKey.Curve).New()
h.Write(data)
digest := h.Sum(nil)
r, s, err := ecdsa.Sign(rand, k.PrivateKey, digest)
if err != nil {
return nil, err
}
sig := make([]byte, intLength(r)+intLength(s))
rest := marshalInt(sig, r)
marshalInt(rest, s)
return &Signature{
Format: k.PublicKey().Type(),
Blob: sig,
}, nil
}
// NewSignerFromKey takes a pointer to rsa, dsa or ecdsa PrivateKey
// returns a corresponding Signer instance. EC keys should use P256,
// P384 or P521.
func NewSignerFromKey(k interface{}) (Signer, error) {
var sshKey Signer
switch t := k.(type) {
case *rsa.PrivateKey:
sshKey = &rsaPrivateKey{t}
case *dsa.PrivateKey:
sshKey = &dsaPrivateKey{t}
case *ecdsa.PrivateKey:
if !supportedEllipticCurve(t.Curve) {
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
}
sshKey = &ecdsaPrivateKey{t}
default:
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
}
return sshKey, nil
}
// NewPublicKey takes a pointer to rsa, dsa or ecdsa PublicKey
// and returns a corresponding ssh PublicKey instance. EC keys should use P256, P384 or P521.
func NewPublicKey(k interface{}) (PublicKey, error) {
var sshKey PublicKey
switch t := k.(type) {
case *rsa.PublicKey:
sshKey = (*rsaPublicKey)(t)
case *ecdsa.PublicKey:
if !supportedEllipticCurve(t.Curve) {
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
}
sshKey = (*ecdsaPublicKey)(t)
case *dsa.PublicKey:
sshKey = (*dsaPublicKey)(t)
default:
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
}
return sshKey, nil
}
// ParsePrivateKey returns a Signer from a PEM encoded private key. It supports
// the same keys as ParseRawPrivateKey.
func ParsePrivateKey(pemBytes []byte) (Signer, error) {
key, err := ParseRawPrivateKey(pemBytes)
if err != nil {
return nil, err
}
return NewSignerFromKey(key)
}
// ParseRawPrivateKey returns a private key from a PEM encoded private key. It
// supports RSA (PKCS#1), DSA (OpenSSL), and ECDSA private keys.
func ParseRawPrivateKey(pemBytes []byte) (interface{}, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("ssh: no key found")
}
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "DSA PRIVATE KEY":
return ParseDSAPrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
}
}
// ParseDSAPrivateKey returns a DSA private key from its ASN.1 DER encoding, as
// specified by the OpenSSL DSA man page.
func ParseDSAPrivateKey(der []byte) (*dsa.PrivateKey, error) {
var k struct {
Version int
P *big.Int
Q *big.Int
G *big.Int
Priv *big.Int
Pub *big.Int
}
rest, err := asn1.Unmarshal(der, &k)
if err != nil {
return nil, errors.New("ssh: failed to parse DSA key: " + err.Error())
}
if len(rest) > 0 {
return nil, errors.New("ssh: garbage after DSA key")
}
return &dsa.PrivateKey{
PublicKey: dsa.PublicKey{
Parameters: dsa.Parameters{
P: k.P,
Q: k.Q,
G: k.G,
},
Y: k.Priv,
},
X: k.Pub,
}, nil
}

306
modules/crypto/ssh/keys_test.go

@ -1,306 +0,0 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"crypto/dsa"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"fmt"
"reflect"
"strings"
"testing"
"github.com/gogits/gogs/modules/crypto/ssh/testdata"
)
func rawKey(pub PublicKey) interface{} {
switch k := pub.(type) {
case *rsaPublicKey:
return (*rsa.PublicKey)(k)
case *dsaPublicKey:
return (*dsa.PublicKey)(k)
case *ecdsaPublicKey:
return (*ecdsa.PublicKey)(k)
case *Certificate:
return k
}
panic("unknown key type")
}
func TestKeyMarshalParse(t *testing.T) {
for _, priv := range testSigners {
pub := priv.PublicKey()
roundtrip, err := ParsePublicKey(pub.Marshal())
if err != nil {
t.Errorf("ParsePublicKey(%T): %v", pub, err)
}
k1 := rawKey(pub)
k2 := rawKey(roundtrip)
if !reflect.DeepEqual(k1, k2) {
t.Errorf("got %#v in roundtrip, want %#v", k2, k1)
}
}
}
func TestUnsupportedCurves(t *testing.T) {
raw, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
if _, err = NewSignerFromKey(raw); err == nil || !strings.Contains(err.Error(), "only P256") {
t.Fatalf("NewPrivateKey should not succeed with P224, got: %v", err)
}
if _, err = NewPublicKey(&raw.PublicKey); err == nil || !strings.Contains(err.Error(), "only P256") {
t.Fatalf("NewPublicKey should not succeed with P224, got: %v", err)
}
}
func TestNewPublicKey(t *testing.T) {
for _, k := range testSigners {
raw := rawKey(k.PublicKey())
// Skip certificates, as NewPublicKey does not support them.
if _, ok := raw.(*Certificate); ok {
continue
}
pub, err := NewPublicKey(raw)
if err != nil {
t.Errorf("NewPublicKey(%#v): %v", raw, err)
}
if !reflect.DeepEqual(k.PublicKey(), pub) {
t.Errorf("NewPublicKey(%#v) = %#v, want %#v", raw, pub, k.PublicKey())
}
}
}
func TestKeySignVerify(t *testing.T) {
for _, priv := range testSigners {
pub := priv.PublicKey()
data := []byte("sign me")
sig, err := priv.Sign(rand.Reader, data)
if err != nil {
t.Fatalf("Sign(%T): %v", priv, err)
}
if err := pub.Verify(data, sig); err != nil {
t.Errorf("publicKey.Verify(%T): %v", priv, err)
}
sig.Blob[5]++
if err := pub.Verify(data, sig); err == nil {
t.Errorf("publicKey.Verify on broken sig did not fail")
}
}
}
func TestParseRSAPrivateKey(t *testing.T) {
key := testPrivateKeys["rsa"]
rsa, ok := key.(*rsa.PrivateKey)
if !ok {
t.Fatalf("got %T, want *rsa.PrivateKey", rsa)
}
if err := rsa.Validate(); err != nil {
t.Errorf("Validate: %v", err)
}
}
func TestParseECPrivateKey(t *testing.T) {
key := testPrivateKeys["ecdsa"]
ecKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
t.Fatalf("got %T, want *ecdsa.PrivateKey", ecKey)
}
if !validateECPublicKey(ecKey.Curve, ecKey.X, ecKey.Y) {
t.Fatalf("public key does not validate.")
}
}
func TestParseDSA(t *testing.T) {
// We actually exercise the ParsePrivateKey codepath here, as opposed to
// using the ParseRawPrivateKey+NewSignerFromKey path that testdata_test.go
// uses.
s, err := ParsePrivateKey(testdata.PEMBytes["dsa"])
if err != nil {
t.Fatalf("ParsePrivateKey returned error: %s", err)
}
data := []byte("sign me")
sig, err := s.Sign(rand.Reader, data)
if err != nil {
t.Fatalf("dsa.Sign: %v", err)
}
if err := s.PublicKey().Verify(data, sig); err != nil {
t.Errorf("Verify failed: %v", err)
}
}
// Tests for authorized_keys parsing.
// getTestKey returns a public key, and its base64 encoding.
func getTestKey() (PublicKey, string) {
k := testPublicKeys["rsa"]
b := &bytes.Buffer{}
e := base64.NewEncoder(base64.StdEncoding, b)
e.Write(k.Marshal())
e.Close()
return k, b.String()
}
func TestMarshalParsePublicKey(t *testing.T) {
pub, pubSerialized := getTestKey()
line := fmt.Sprintf("%s %s user@host", pub.Type(), pubSerialized)
authKeys := MarshalAuthorizedKey(pub)
actualFields := strings.Fields(string(authKeys))
if len(actualFields) == 0 {
t.Fatalf("failed authKeys: %v", authKeys)
}
// drop the comment
expectedFields := strings.Fields(line)[0:2]
if !reflect.DeepEqual(actualFields, expectedFields) {
t.Errorf("got %v, expected %v", actualFields, expectedFields)
}
actPub, _, _, _, err := ParseAuthorizedKey([]byte(line))
if err != nil {
t.Fatalf("cannot parse %v: %v", line, err)
}
if !reflect.DeepEqual(actPub, pub) {
t.Errorf("got %v, expected %v", actPub, pub)
}
}
type authResult struct {
pubKey PublicKey
options []string
comments string
rest string
ok bool
}
func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []authResult) {
rest := authKeys
var values []authResult
for len(rest) > 0 {
var r authResult
var err error
r.pubKey, r.comments, r.options, rest, err = ParseAuthorizedKey(rest)
r.ok = (err == nil)
t.Log(err)
r.rest = string(rest)
values = append(values, r)
}
if !reflect.DeepEqual(values, expected) {
t.Errorf("got %#v, expected %#v", values, expected)
}
}
func TestAuthorizedKeyBasic(t *testing.T) {
pub, pubSerialized := getTestKey()
line := "ssh-rsa " + pubSerialized + " user@host"
testAuthorizedKeys(t, []byte(line),
[]authResult{
{pub, nil, "user@host", "", true},
})
}
func TestAuth(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithOptions := []string{
`# comments to ignore before any keys...`,
``,
`env="HOME=/home/root",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`,
`# comments to ignore, along with a blank line`,
``,
`env="HOME=/home/root2" ssh-rsa ` + pubSerialized + ` user2@host2`,
``,
`# more comments, plus a invalid entry`,
`ssh-rsa data-that-will-not-parse user@host3`,
}
for _, eol := range []string{"\n", "\r\n"} {
authOptions := strings.Join(authWithOptions, eol)
rest2 := strings.Join(authWithOptions[3:], eol)
rest3 := strings.Join(authWithOptions[6:], eol)
testAuthorizedKeys(t, []byte(authOptions), []authResult{
{pub, []string{`env="HOME=/home/root"`, "no-port-forwarding"}, "user@host", rest2, true},
{pub, []string{`env="HOME=/home/root2"`}, "user2@host2", rest3, true},
{nil, nil, "", "", false},
})
}
}
func TestAuthWithQuotedSpaceInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedSpaceInEnv := []byte(`env="HOME=/home/root dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []authResult{
{pub, []string{`env="HOME=/home/root dir"`, "no-port-forwarding"}, "user@host", "", true},
})
}
func TestAuthWithQuotedCommaInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedCommaInEnv := []byte(`env="HOME=/home/root,dir",no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []authResult{
{pub, []string{`env="HOME=/home/root,dir"`, "no-port-forwarding"}, "user@host", "", true},
})
}
func TestAuthWithQuotedQuoteInEnv(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithQuotedQuoteInEnv := []byte(`env="HOME=/home/\"root dir",no-port-forwarding` + "\t" + `ssh-rsa` + "\t" + pubSerialized + ` user@host`)
authWithDoubleQuotedQuote := []byte(`no-port-forwarding,env="HOME=/home/ \"root dir\"" ssh-rsa ` + pubSerialized + "\t" + `user@host`)
testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []authResult{
{pub, []string{`env="HOME=/home/\"root dir"`, "no-port-forwarding"}, "user@host", "", true},
})
testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []authResult{
{pub, []string{"no-port-forwarding", `env="HOME=/home/ \"root dir\""`}, "user@host", "", true},
})
}
func TestAuthWithInvalidSpace(t *testing.T) {
_, pubSerialized := getTestKey()
authWithInvalidSpace := []byte(`env="HOME=/home/root dir", no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host
#more to follow but still no valid keys`)
testAuthorizedKeys(t, []byte(authWithInvalidSpace), []authResult{
{nil, nil, "", "", false},
})
}
func TestAuthWithMissingQuote(t *testing.T) {
pub, pubSerialized := getTestKey()
authWithMissingQuote := []byte(`env="HOME=/home/root,no-port-forwarding ssh-rsa ` + pubSerialized + ` user@host
env="HOME=/home/root",shared-control ssh-rsa ` + pubSerialized + ` user@host`)
testAuthorizedKeys(t, []byte(authWithMissingQuote), []authResult{
{pub, []string{`env="HOME=/home/root"`, `shared-control`}, "user@host", "", true},
})
}
func TestInvalidEntry(t *testing.T) {
authInvalid := []byte(`ssh-rsa`)
_, _, _, _, err := ParseAuthorizedKey(authInvalid)
if err == nil {
t.Errorf("got valid entry for %q", authInvalid)
}
}

57
modules/crypto/ssh/mac.go

@ -1,57 +0,0 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
// Message authentication support
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"hash"
)
type macMode struct {
keySize int
new func(key []byte) hash.Hash
}
// truncatingMAC wraps around a hash.Hash and truncates the output digest to
// a given size.
type truncatingMAC struct {
length int
hmac hash.Hash
}
func (t truncatingMAC) Write(data []byte) (int, error) {
return t.hmac.Write(data)
}
func (t truncatingMAC) Sum(in []byte) []byte {
out := t.hmac.Sum(in)
return out[:len(in)+t.length]
}
func (t truncatingMAC) Reset() {
t.hmac.Reset()
}
func (t truncatingMAC) Size() int {
return t.length
}
func (t truncatingMAC) BlockSize() int { return t.hmac.BlockSize() }
var macModes = map[string]*macMode{
"hmac-sha2-256": {32, func(key []byte) hash.Hash {
return hmac.New(sha256.New, key)
}},
"hmac-sha1": {20, func(key []byte) hash.Hash {
return hmac.New(sha1.New, key)
}},
"hmac-sha1-96": {20, func(key []byte) hash.Hash {
return truncatingMAC{12, hmac.New(sha1.New, key)}
}},
}

110
modules/crypto/ssh/mempipe_test.go

@ -1,110 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"io"
"sync"
"testing"
)
// An in-memory packetConn. It is safe to call Close and writePacket
// from different goroutines.
type memTransport struct {
eof bool
pending [][]byte
write *memTransport
sync.Mutex
*sync.Cond
}
func (t *memTransport) readPacket() ([]byte, error) {
t.Lock()
defer t.Unlock()
for {
if len(t.pending) > 0 {
r := t.pending[0]
t.pending = t.pending[1:]
return r, nil
}
if t.eof {
return nil, io.EOF
}
t.Cond.Wait()
}
}
func (t *memTransport) closeSelf() error {
t.Lock()
defer t.Unlock()
if t.eof {
return io.EOF
}
t.eof = true
t.Cond.Broadcast()
return nil
}
func (t *memTransport) Close() error {
err := t.write.closeSelf()
t.closeSelf()
return err
}
func (t *memTransport) writePacket(p []byte) error {
t.write.Lock()
defer t.write.Unlock()
if t.write.eof {
return io.EOF
}
c := make([]byte, len(p))
copy(c, p)
t.write.pending = append(t.write.pending, c)
t.write.Cond.Signal()
return nil
}
func memPipe() (a, b packetConn) {
t1 := memTransport{}
t2 := memTransport{}
t1.write = &t2
t2.write = &t1
t1.Cond = sync.NewCond(&t1.Mutex)
t2.Cond = sync.NewCond(&t2.Mutex)
return &t1, &t2
}
func TestMemPipe(t *testing.T) {
a, b := memPipe()
if err := a.writePacket([]byte{42}); err != nil {
t.Fatalf("writePacket: %v", err)
}
if err := a.Close(); err != nil {
t.Fatal("Close: ", err)
}
p, err := b.readPacket()
if err != nil {
t.Fatal("readPacket: ", err)
}
if len(p) != 1 || p[0] != 42 {
t.Fatalf("got %v, want {42}", p)
}
p, err = b.readPacket()
if err != io.EOF {
t.Fatalf("got %v, %v, want EOF", p, err)
}
}
func TestDoubleClose(t *testing.T) {
a, _ := memPipe()
err := a.Close()
if err != nil {
t.Errorf("Close: %v", err)
}
err = a.Close()
if err != io.EOF {
t.Errorf("expect EOF on double close.")
}
}

725
modules/crypto/ssh/messages.go

@ -1,725 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"reflect"
"strconv"
)
// These are SSH message type numbers. They are scattered around several
// documents but many were taken from [SSH-PARAMETERS].
const (
msgIgnore = 2
msgUnimplemented = 3
msgDebug = 4
msgNewKeys = 21
// Standard authentication messages
msgUserAuthSuccess = 52
msgUserAuthBanner = 53
)
// SSH messages:
//
// These structures mirror the wire format of the corresponding SSH messages.
// They are marshaled using reflection with the marshal and unmarshal functions
// in this file. The only wrinkle is that a final member of type []byte with a
// ssh tag of "rest" receives the remainder of a packet when unmarshaling.
// See RFC 4253, section 11.1.
const msgDisconnect = 1
// disconnectMsg is the message that signals a disconnect. It is also
// the error type returned from mux.Wait()
type disconnectMsg struct {
Reason uint32 `sshtype:"1"`
Message string
Language string
}
func (d *disconnectMsg) Error() string {
return fmt.Sprintf("ssh: disconnect reason %d: %s", d.Reason, d.Message)
}
// See RFC 4253, section 7.1.
const msgKexInit = 20
type kexInitMsg struct {
Cookie [16]byte `sshtype:"20"`
KexAlgos []string
ServerHostKeyAlgos []string
CiphersClientServer []string
CiphersServerClient []string
MACsClientServer []string
MACsServerClient []string
CompressionClientServer []string
CompressionServerClient []string
LanguagesClientServer []string
LanguagesServerClient []string
FirstKexFollows bool
Reserved uint32
}
// See RFC 4253, section 8.
// Diffie-Helman
const msgKexDHInit = 30
type kexDHInitMsg struct {
X *big.Int `sshtype:"30"`
}
const msgKexECDHInit = 30
type kexECDHInitMsg struct {
ClientPubKey []byte `sshtype:"30"`
}
const msgKexECDHReply = 31
type kexECDHReplyMsg struct {
HostKey []byte `sshtype:"31"`
EphemeralPubKey []byte
Signature []byte
}
const msgKexDHReply = 31
type kexDHReplyMsg struct {
HostKey []byte `sshtype:"31"`
Y *big.Int
Signature []byte
}
// See RFC 4253, section 10.
const msgServiceRequest = 5
type serviceRequestMsg struct {
Service string `sshtype:"5"`
}
// See RFC 4253, section 10.
const msgServiceAccept = 6
type serviceAcceptMsg struct {
Service string `sshtype:"6"`
}
// See RFC 4252, section 5.
const msgUserAuthRequest = 50
type userAuthRequestMsg struct {
User string `sshtype:"50"`
Service string
Method string
Payload []byte `ssh:"rest"`
}
// See RFC 4252, section 5.1
const msgUserAuthFailure = 51
type userAuthFailureMsg struct {
Methods []string `sshtype:"51"`
PartialSuccess bool
}
// See RFC 4256, section 3.2
const msgUserAuthInfoRequest = 60
const msgUserAuthInfoResponse = 61
type userAuthInfoRequestMsg struct {
User string `sshtype:"60"`
Instruction string
DeprecatedLanguage string
NumPrompts uint32
Prompts []byte `ssh:"rest"`
}
// See RFC 4254, section 5.1.
const msgChannelOpen = 90
type channelOpenMsg struct {
ChanType string `sshtype:"90"`
PeersId uint32
PeersWindow uint32
MaxPacketSize uint32
TypeSpecificData []byte `ssh:"rest"`
}
const msgChannelExtendedData = 95
const msgChannelData = 94
// See RFC 4254, section 5.1.
const msgChannelOpenConfirm = 91
type channelOpenConfirmMsg struct {
PeersId uint32 `sshtype:"91"`
MyId uint32
MyWindow uint32
MaxPacketSize uint32
TypeSpecificData []byte `ssh:"rest"`
}
// See RFC 4254, section 5.1.
const msgChannelOpenFailure = 92
type channelOpenFailureMsg struct {
PeersId uint32 `sshtype:"92"`
Reason RejectionReason
Message string
Language string
}
const msgChannelRequest = 98
type channelRequestMsg struct {
PeersId uint32 `sshtype:"98"`
Request string
WantReply bool
RequestSpecificData []byte `ssh:"rest"`
}
// See RFC 4254, section 5.4.
const msgChannelSuccess = 99
type channelRequestSuccessMsg struct {
PeersId uint32 `sshtype:"99"`
}
// See RFC 4254, section 5.4.
const msgChannelFailure = 100
type channelRequestFailureMsg struct {
PeersId uint32 `sshtype:"100"`
}
// See RFC 4254, section 5.3
const msgChannelClose = 97
type channelCloseMsg struct {
PeersId uint32 `sshtype:"97"`
}
// See RFC 4254, section 5.3
const msgChannelEOF = 96
type channelEOFMsg struct {
PeersId uint32 `sshtype:"96"`
}
// See RFC 4254, section 4
const msgGlobalRequest = 80
type globalRequestMsg struct {
Type string `sshtype:"80"`
WantReply bool
Data []byte `ssh:"rest"`
}
// See RFC 4254, section 4
const msgRequestSuccess = 81
type globalRequestSuccessMsg struct {
Data []byte `ssh:"rest" sshtype:"81"`
}
// See RFC 4254, section 4
const msgRequestFailure = 82
type globalRequestFailureMsg struct {
Data []byte `ssh:"rest" sshtype:"82"`
}
// See RFC 4254, section 5.2
const msgChannelWindowAdjust = 93
type windowAdjustMsg struct {
PeersId uint32 `sshtype:"93"`
AdditionalBytes uint32
}
// See RFC 4252, section 7
const msgUserAuthPubKeyOk = 60
type userAuthPubKeyOkMsg struct {
Algo string `sshtype:"60"`
PubKey []byte
}
// typeTag returns the type byte for the given type. The type should
// be struct.
func typeTag(structType reflect.Type) byte {
var tag byte
var tagStr string
tagStr = structType.Field(0).Tag.Get("sshtype")
i, err := strconv.Atoi(tagStr)
if err == nil {
tag = byte(i)
}
return tag
}
func fieldError(t reflect.Type, field int, problem string) error {
if problem != "" {
problem = ": " + problem
}
return fmt.Errorf("ssh: unmarshal error for field %s of type %s%s", t.Field(field).Name, t.Name(), problem)
}
var errShortRead = errors.New("ssh: short read")
// Unmarshal parses data in SSH wire format into a structure. The out
// argument should be a pointer to struct. If the first member of the
// struct has the "sshtype" tag set to a number in decimal, the packet
// must start that number. In case of error, Unmarshal returns a
// ParseError or UnexpectedMessageError.
func Unmarshal(data []byte, out interface{}) error {
v := reflect.ValueOf(out).Elem()
structType := v.Type()
expectedType := typeTag(structType)
if len(data) == 0 {
return parseError(expectedType)
}
if expectedType > 0 {
if data[0] != expectedType {
return unexpectedMessageError(expectedType, data[0])
}
data = data[1:]
}
var ok bool
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
t := field.Type()
switch t.Kind() {
case reflect.Bool:
if len(data) < 1 {
return errShortRead
}
field.SetBool(data[0] != 0)
data = data[1:]
case reflect.Array:
if t.Elem().Kind() != reflect.Uint8 {
return fieldError(structType, i, "array of unsupported type")
}
if len(data) < t.Len() {
return errShortRead
}
for j, n := 0, t.Len(); j < n; j++ {
field.Index(j).Set(reflect.ValueOf(data[j]))
}
data = data[t.Len():]
case reflect.Uint64:
var u64 uint64
if u64, data, ok = parseUint64(data); !ok {
return errShortRead
}
field.SetUint(u64)
case reflect.Uint32:
var u32 uint32
if u32, data, ok = parseUint32(data); !ok {
return errShortRead
}
field.SetUint(uint64(u32))
case reflect.Uint8:
if len(data) < 1 {
return errShortRead
}
field.SetUint(uint64(data[0]))
data = data[1:]
case reflect.String:
var s []byte
if s, data, ok = parseString(data); !ok {
return fieldError(structType, i, "")
}
field.SetString(string(s))
case reflect.Slice:
switch t.Elem().Kind() {
case reflect.Uint8:
if structType.Field(i).Tag.Get("ssh") == "rest" {
field.Set(reflect.ValueOf(data))
data = nil
} else {
var s []byte
if s, data, ok = parseString(data); !ok {
return errShortRead
}
field.Set(reflect.ValueOf(s))
}
case reflect.String:
var nl []string
if nl, data, ok = parseNameList(data); !ok {
return errShortRead
}
field.Set(reflect.ValueOf(nl))
default:
return fieldError(structType, i, "slice of unsupported type")
}
case reflect.Ptr:
if t == bigIntType {
var n *big.Int
if n, data, ok = parseInt(data); !ok {
return errShortRead
}
field.Set(reflect.ValueOf(n))
} else {
return fieldError(structType, i, "pointer to unsupported type")
}
default:
return fieldError(structType, i, "unsupported type")
}
}
if len(data) != 0 {
return parseError(expectedType)
}
return nil
}
// Marshal serializes the message in msg to SSH wire format. The msg
// argument should be a struct or pointer to struct. If the first
// member has the "sshtype" tag set to a number in decimal, that
// number is prepended to the result. If the last of member has the
// "ssh" tag set to "rest", its contents are appended to the output.
func Marshal(msg interface{}) []byte {
out := make([]byte, 0, 64)
return marshalStruct(out, msg)
}
func marshalStruct(out []byte, msg interface{}) []byte {
v := reflect.Indirect(reflect.ValueOf(msg))
msgType := typeTag(v.Type())
if msgType > 0 {
out = append(out, msgType)
}
for i, n := 0, v.NumField(); i < n; i++ {
field := v.Field(i)
switch t := field.Type(); t.Kind() {
case reflect.Bool:
var v uint8
if field.Bool() {
v = 1
}
out = append(out, v)
case reflect.Array:
if t.Elem().Kind() != reflect.Uint8 {
panic(fmt.Sprintf("array of non-uint8 in field %d: %T", i, field.Interface()))
}
for j, l := 0, t.Len(); j < l; j++ {
out = append(out, uint8(field.Index(j).Uint()))
}
case reflect.Uint32:
out = appendU32(out, uint32(field.Uint()))
case reflect.Uint64:
out = appendU64(out, uint64(field.Uint()))
case reflect.Uint8:
out = append(out, uint8(field.Uint()))
case reflect.String:
s := field.String()
out = appendInt(out, len(s))
out = append(out, s...)
case reflect.Slice:
switch t.Elem().Kind() {
case reflect.Uint8:
if v.Type().Field(i).Tag.Get("ssh") != "rest" {
out = appendInt(out, field.Len())
}
out = append(out, field.Bytes()...)
case reflect.String:
offset := len(out)
out = appendU32(out, 0)
if n := field.Len(); n > 0 {
for j := 0; j < n; j++ {
f := field.Index(j)
if j != 0 {
out = append(out, ',')
}
out = append(out, f.String()...)
}
// overwrite length value
binary.BigEndian.PutUint32(out[offset:], uint32(len(out)-offset-4))
}
default:
panic(fmt.Sprintf("slice of unknown type in field %d: %T", i, field.Interface()))
}
case reflect.Ptr:
if t == bigIntType {
var n *big.Int
nValue := reflect.ValueOf(&n)
nValue.Elem().Set(field)
needed := intLength(n)
oldLength := len(out)
if cap(out)-len(out) < needed {
newOut := make([]byte, len(out), 2*(len(out)+needed))
copy(newOut, out)
out = newOut
}
out = out[:oldLength+needed]
marshalInt(out[oldLength:], n)
} else {
panic(fmt.Sprintf("pointer to unknown type in field %d: %T", i, field.Interface()))
}
}
}
return out
}
var bigOne = big.NewInt(1)
func parseString(in []byte) (out, rest []byte, ok bool) {
if len(in) < 4 {
return
}
length := binary.BigEndian.Uint32(in)
in = in[4:]
if uint32(len(in)) < length {
return
}
out = in[:length]
rest = in[length:]
ok = true
return
}
var (
comma = []byte{','}
emptyNameList = []string{}
)
func parseNameList(in []byte) (out []string, rest []byte, ok bool) {
contents, rest, ok := parseString(in)
if !ok {
return
}
if len(contents) == 0 {
out = emptyNameList
return
}
parts := bytes.Split(contents, comma)
out = make([]string, len(parts))
for i, part := range parts {
out[i] = string(part)
}
return
}
func parseInt(in []byte) (out *big.Int, rest []byte, ok bool) {
contents, rest, ok := parseString(in)
if !ok {
return
}
out = new(big.Int)
if len(contents) > 0 && contents[0]&0x80 == 0x80 {
// This is a negative number
notBytes := make([]byte, len(contents))
for i := range notBytes {
notBytes[i] = ^contents[i]
}
out.SetBytes(notBytes)
out.Add(out, bigOne)
out.Neg(out)
} else {
// Positive number
out.SetBytes(contents)
}
ok = true
return
}
func parseUint32(in []byte) (uint32, []byte, bool) {
if len(in) < 4 {
return 0, nil, false
}
return binary.BigEndian.Uint32(in), in[4:], true
}
func parseUint64(in []byte) (uint64, []byte, bool) {
if len(in) < 8 {
return 0, nil, false
}
return binary.BigEndian.Uint64(in), in[8:], true
}
func intLength(n *big.Int) int {
length := 4 /* length bytes */
if n.Sign() < 0 {
nMinus1 := new(big.Int).Neg(n)
nMinus1.Sub(nMinus1, bigOne)
bitLen := nMinus1.BitLen()
if bitLen%8 == 0 {
// The number will need 0xff padding
length++
}
length += (bitLen + 7) / 8
} else if n.Sign() == 0 {
// A zero is the zero length string
} else {
bitLen := n.BitLen()
if bitLen%8 == 0 {
// The number will need 0x00 padding
length++
}
length += (bitLen + 7) / 8
}
return length
}
func marshalUint32(to []byte, n uint32) []byte {
binary.BigEndian.PutUint32(to, n)
return to[4:]
}
func marshalUint64(to []byte, n uint64) []byte {
binary.BigEndian.PutUint64(to, n)
return to[8:]
}
func marshalInt(to []byte, n *big.Int) []byte {
lengthBytes := to
to = to[4:]
length := 0
if n.Sign() < 0 {
// A negative number has to be converted to two's-complement
// form. So we'll subtract 1 and invert. If the
// most-significant-bit isn't set then we'll need to pad the
// beginning with 0xff in order to keep the number negative.
nMinus1 := new(big.Int).Neg(n)
nMinus1.Sub(nMinus1, bigOne)
bytes := nMinus1.Bytes()
for i := range bytes {
bytes[i] ^= 0xff
}
if len(bytes) == 0 || bytes[0]&0x80 == 0 {
to[0] = 0xff
to = to[1:]
length++
}
nBytes := copy(to, bytes)
to = to[nBytes:]
length += nBytes
} else if n.Sign() == 0 {
// A zero is the zero length string
} else {
bytes := n.Bytes()
if len(bytes) > 0 && bytes[0]&0x80 != 0 {
// We'll have to pad this with a 0x00 in order to
// stop it looking like a negative number.
to[0] = 0
to = to[1:]
length++
}
nBytes := copy(to, bytes)
to = to[nBytes:]
length += nBytes
}
lengthBytes[0] = byte(length >> 24)
lengthBytes[1] = byte(length >> 16)
lengthBytes[2] = byte(length >> 8)
lengthBytes[3] = byte(length)
return to
}
func writeInt(w io.Writer, n *big.Int) {
length := intLength(n)
buf := make([]byte, length)
marshalInt(buf, n)
w.Write(buf)
}
func writeString(w io.Writer, s []byte) {
var lengthBytes [4]byte
lengthBytes[0] = byte(len(s) >> 24)
lengthBytes[1] = byte(len(s) >> 16)
lengthBytes[2] = byte(len(s) >> 8)
lengthBytes[3] = byte(len(s))
w.Write(lengthBytes[:])
w.Write(s)
}
func stringLength(n int) int {
return 4 + n
}
func marshalString(to []byte, s []byte) []byte {
to[0] = byte(len(s) >> 24)
to[1] = byte(len(s) >> 16)
to[2] = byte(len(s) >> 8)
to[3] = byte(len(s))
to = to[4:]
copy(to, s)
return to[len(s):]
}
var bigIntType = reflect.TypeOf((*big.Int)(nil))
// Decode a packet into its corresponding message.
func decode(packet []byte) (interface{}, error) {
var msg interface{}
switch packet[0] {
case msgDisconnect:
msg = new(disconnectMsg)
case msgServiceRequest:
msg = new(serviceRequestMsg)
case msgServiceAccept:
msg = new(serviceAcceptMsg)
case msgKexInit:
msg = new(kexInitMsg)
case msgKexDHInit:
msg = new(kexDHInitMsg)
case msgKexDHReply:
msg = new(kexDHReplyMsg)
case msgUserAuthRequest:
msg = new(userAuthRequestMsg)
case msgUserAuthFailure:
msg = new(userAuthFailureMsg)
case msgUserAuthPubKeyOk:
msg = new(userAuthPubKeyOkMsg)
case msgGlobalRequest:
msg = new(globalRequestMsg)
case msgRequestSuccess:
msg = new(globalRequestSuccessMsg)
case msgRequestFailure:
msg = new(globalRequestFailureMsg)
case msgChannelOpen:
msg = new(channelOpenMsg)
case msgChannelOpenConfirm:
msg = new(channelOpenConfirmMsg)
case msgChannelOpenFailure:
msg = new(channelOpenFailureMsg)
case msgChannelWindowAdjust:
msg = new(windowAdjustMsg)
case msgChannelEOF:
msg = new(channelEOFMsg)
case msgChannelClose:
msg = new(channelCloseMsg)
case msgChannelRequest:
msg = new(channelRequestMsg)
case msgChannelSuccess:
msg = new(channelRequestSuccessMsg)
case msgChannelFailure:
msg = new(channelRequestFailureMsg)
default:
return nil, unexpectedMessageError(0, packet[0])
}
if err := Unmarshal(packet, msg); err != nil {
return nil, err
}
return msg, nil
}

254
modules/crypto/ssh/messages_test.go

@ -1,254 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"math/big"
"math/rand"
"reflect"
"testing"
"testing/quick"
)
var intLengthTests = []struct {
val, length int
}{
{0, 4 + 0},
{1, 4 + 1},
{127, 4 + 1},
{128, 4 + 2},
{-1, 4 + 1},
}
func TestIntLength(t *testing.T) {
for _, test := range intLengthTests {
v := new(big.Int).SetInt64(int64(test.val))
length := intLength(v)
if length != test.length {
t.Errorf("For %d, got length %d but expected %d", test.val, length, test.length)
}
}
}
type msgAllTypes struct {
Bool bool `sshtype:"21"`
Array [16]byte
Uint64 uint64
Uint32 uint32
Uint8 uint8
String string
Strings []string
Bytes []byte
Int *big.Int
Rest []byte `ssh:"rest"`
}
func (t *msgAllTypes) Generate(rand *rand.Rand, size int) reflect.Value {
m := &msgAllTypes{}
m.Bool = rand.Intn(2) == 1
randomBytes(m.Array[:], rand)
m.Uint64 = uint64(rand.Int63n(1<<63 - 1))
m.Uint32 = uint32(rand.Intn((1 << 31) - 1))
m.Uint8 = uint8(rand.Intn(1 << 8))
m.String = string(m.Array[:])
m.Strings = randomNameList(rand)
m.Bytes = m.Array[:]
m.Int = randomInt(rand)
m.Rest = m.Array[:]
return reflect.ValueOf(m)
}
func TestMarshalUnmarshal(t *testing.T) {
rand := rand.New(rand.NewSource(0))
iface := &msgAllTypes{}
ty := reflect.ValueOf(iface).Type()
n := 100
if testing.Short() {
n = 5
}
for j := 0; j < n; j++ {
v, ok := quick.Value(ty, rand)
if !ok {
t.Errorf("failed to create value")
break
}
m1 := v.Elem().Interface()
m2 := iface
marshaled := Marshal(m1)
if err := Unmarshal(marshaled, m2); err != nil {
t.Errorf("Unmarshal %#v: %s", m1, err)
break
}
if !reflect.DeepEqual(v.Interface(), m2) {
t.Errorf("got: %#v\nwant:%#v\n%x", m2, m1, marshaled)
break
}
}
}
func TestUnmarshalEmptyPacket(t *testing.T) {
var b []byte
var m channelRequestSuccessMsg
if err := Unmarshal(b, &m); err == nil {
t.Fatalf("unmarshal of empty slice succeeded")
}
}
func TestUnmarshalUnexpectedPacket(t *testing.T) {
type S struct {
I uint32 `sshtype:"43"`
S string
B bool
}
s := S{11, "hello", true}
packet := Marshal(s)
packet[0] = 42
roundtrip := S{}
err := Unmarshal(packet, &roundtrip)
if err == nil {
t.Fatal("expected error, not nil")
}
}
func TestMarshalPtr(t *testing.T) {
s := struct {
S string
}{"hello"}
m1 := Marshal(s)
m2 := Marshal(&s)
if !bytes.Equal(m1, m2) {
t.Errorf("got %q, want %q for marshaled pointer", m2, m1)
}
}
func TestBareMarshalUnmarshal(t *testing.T) {
type S struct {
I uint32
S string
B bool
}
s := S{42, "hello", true}
packet := Marshal(s)
roundtrip := S{}
Unmarshal(packet, &roundtrip)
if !reflect.DeepEqual(s, roundtrip) {
t.Errorf("got %#v, want %#v", roundtrip, s)
}
}
func TestBareMarshal(t *testing.T) {
type S2 struct {
I uint32
}
s := S2{42}
packet := Marshal(s)
i, rest, ok := parseUint32(packet)
if len(rest) > 0 || !ok {
t.Errorf("parseInt(%q): parse error", packet)
}
if i != s.I {
t.Errorf("got %d, want %d", i, s.I)
}
}
func TestUnmarshalShortKexInitPacket(t *testing.T) {
// This used to panic.
// Issue 11348
packet := []byte{0x14, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0xff, 0xff, 0xff, 0xff}
kim := &kexInitMsg{}
if err := Unmarshal(packet, kim); err == nil {
t.Error("truncated packet unmarshaled without error")
}
}
func randomBytes(out []byte, rand *rand.Rand) {
for i := 0; i < len(out); i++ {
out[i] = byte(rand.Int31())
}
}
func randomNameList(rand *rand.Rand) []string {
ret := make([]string, rand.Int31()&15)
for i := range ret {
s := make([]byte, 1+(rand.Int31()&15))
for j := range s {
s[j] = 'a' + uint8(rand.Int31()&15)
}
ret[i] = string(s)
}
return ret
}
func randomInt(rand *rand.Rand) *big.Int {
return new(big.Int).SetInt64(int64(int32(rand.Uint32())))
}
func (*kexInitMsg) Generate(rand *rand.Rand, size int) reflect.Value {
ki := &kexInitMsg{}
randomBytes(ki.Cookie[:], rand)
ki.KexAlgos = randomNameList(rand)
ki.ServerHostKeyAlgos = randomNameList(rand)
ki.CiphersClientServer = randomNameList(rand)
ki.CiphersServerClient = randomNameList(rand)
ki.MACsClientServer = randomNameList(rand)
ki.MACsServerClient = randomNameList(rand)
ki.CompressionClientServer = randomNameList(rand)
ki.CompressionServerClient = randomNameList(rand)
ki.LanguagesClientServer = randomNameList(rand)
ki.LanguagesServerClient = randomNameList(rand)
if rand.Int31()&1 == 1 {
ki.FirstKexFollows = true
}
return reflect.ValueOf(ki)
}
func (*kexDHInitMsg) Generate(rand *rand.Rand, size int) reflect.Value {
dhi := &kexDHInitMsg{}
dhi.X = randomInt(rand)
return reflect.ValueOf(dhi)
}
var (
_kexInitMsg = new(kexInitMsg).Generate(rand.New(rand.NewSource(0)), 10).Elem().Interface()
_kexDHInitMsg = new(kexDHInitMsg).Generate(rand.New(rand.NewSource(0)), 10).Elem().Interface()
_kexInit = Marshal(_kexInitMsg)
_kexDHInit = Marshal(_kexDHInitMsg)
)
func BenchmarkMarshalKexInitMsg(b *testing.B) {
for i := 0; i < b.N; i++ {
Marshal(_kexInitMsg)
}
}
func BenchmarkUnmarshalKexInitMsg(b *testing.B) {
m := new(kexInitMsg)
for i := 0; i < b.N; i++ {
Unmarshal(_kexInit, m)
}
}
func BenchmarkMarshalKexDHInitMsg(b *testing.B) {
for i := 0; i < b.N; i++ {
Marshal(_kexDHInitMsg)
}
}
func BenchmarkUnmarshalKexDHInitMsg(b *testing.B) {
m := new(kexDHInitMsg)
for i := 0; i < b.N; i++ {
Unmarshal(_kexDHInit, m)
}
}

356
modules/crypto/ssh/mux.go

@ -1,356 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"encoding/binary"
"fmt"
"io"
"log"
"sync"
"sync/atomic"
)
// debugMux, if set, causes messages in the connection protocol to be
// logged.
const debugMux = false
// chanList is a thread safe channel list.
type chanList struct {
// protects concurrent access to chans
sync.Mutex
// chans are indexed by the local id of the channel, which the
// other side should send in the PeersId field.
chans []*channel
// This is a debugging aid: it offsets all IDs by this
// amount. This helps distinguish otherwise identical
// server/client muxes
offset uint32
}
// Assigns a channel ID to the given channel.
func (c *chanList) add(ch *channel) uint32 {
c.Lock()
defer c.Unlock()
for i := range c.chans {
if c.chans[i] == nil {
c.chans[i] = ch
return uint32(i) + c.offset
}
}
c.chans = append(c.chans, ch)
return uint32(len(c.chans)-1) + c.offset
}
// getChan returns the channel for the given ID.
func (c *chanList) getChan(id uint32) *channel {
id -= c.offset
c.Lock()
defer c.Unlock()
if id < uint32(len(c.chans)) {
return c.chans[id]
}
return nil
}
func (c *chanList) remove(id uint32) {
id -= c.offset
c.Lock()
if id < uint32(len(c.chans)) {
c.chans[id] = nil
}
c.Unlock()
}
// dropAll forgets all channels it knows, returning them in a slice.
func (c *chanList) dropAll() []*channel {
c.Lock()
defer c.Unlock()
var r []*channel
for _, ch := range c.chans {
if ch == nil {
continue
}
r = append(r, ch)
}
c.chans = nil
return r
}
// mux represents the state for the SSH connection protocol, which
// multiplexes many channels onto a single packet transport.
type mux struct {
conn packetConn
chanList chanList
incomingChannels chan NewChannel
globalSentMu sync.Mutex
globalResponses chan interface{}
incomingRequests chan *Request
errCond *sync.Cond
err error
}
// When debugging, each new chanList instantiation has a different
// offset.
var globalOff uint32
func (m *mux) Wait() error {
m.errCond.L.Lock()
defer m.errCond.L.Unlock()
for m.err == nil {
m.errCond.Wait()
}
return m.err
}
// newMux returns a mux that runs over the given connection.
func newMux(p packetConn) *mux {
m := &mux{
conn: p,
incomingChannels: make(chan NewChannel, 16),
globalResponses: make(chan interface{}, 1),
incomingRequests: make(chan *Request, 16),
errCond: newCond(),
}
if debugMux {
m.chanList.offset = atomic.AddUint32(&globalOff, 1)
}
go m.loop()
return m
}
func (m *mux) sendMessage(msg interface{}) error {
p := Marshal(msg)
return m.conn.writePacket(p)
}
func (m *mux) SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, error) {
if wantReply {
m.globalSentMu.Lock()
defer m.globalSentMu.Unlock()
}
if err := m.sendMessage(globalRequestMsg{
Type: name,
WantReply: wantReply,
Data: payload,
}); err != nil {
return false, nil, err
}
if !wantReply {
return false, nil, nil
}
msg, ok := <-m.globalResponses
if !ok {
return false, nil, io.EOF
}
switch msg := msg.(type) {
case *globalRequestFailureMsg:
return false, msg.Data, nil
case *globalRequestSuccessMsg:
return true, msg.Data, nil
default:
return false, nil, fmt.Errorf("ssh: unexpected response to request: %#v", msg)
}
}
// ackRequest must be called after processing a global request that
// has WantReply set.
func (m *mux) ackRequest(ok bool, data []byte) error {
if ok {
return m.sendMessage(globalRequestSuccessMsg{Data: data})
}
return m.sendMessage(globalRequestFailureMsg{Data: data})
}
// TODO(hanwen): Disconnect is a transport layer message. We should
// probably send and receive Disconnect somewhere in the transport
// code.
// Disconnect sends a disconnect message.
func (m *mux) Disconnect(reason uint32, message string) error {
return m.sendMessage(disconnectMsg{
Reason: reason,
Message: message,
})
}
func (m *mux) Close() error {
return m.conn.Close()
}
// loop runs the connection machine. It will process packets until an
// error is encountered. To synchronize on loop exit, use mux.Wait.
func (m *mux) loop() {
var err error
for err == nil {
err = m.onePacket()
}
for _, ch := range m.chanList.dropAll() {
ch.close()
}
close(m.incomingChannels)
close(m.incomingRequests)
close(m.globalResponses)
m.conn.Close()
m.errCond.L.Lock()
m.err = err
m.errCond.Broadcast()
m.errCond.L.Unlock()
if debugMux {
log.Println("loop exit", err)
}
}
// onePacket reads and processes one packet.
func (m *mux) onePacket() error {
packet, err := m.conn.readPacket()
if err != nil {
return err
}
if debugMux {
if packet[0] == msgChannelData || packet[0] == msgChannelExtendedData {
log.Printf("decoding(%d): data packet - %d bytes", m.chanList.offset, len(packet))
} else {
p, _ := decode(packet)
log.Printf("decoding(%d): %d %#v - %d bytes", m.chanList.offset, packet[0], p, len(packet))
}
}
switch packet[0] {
case msgNewKeys:
// Ignore notification of key change.
return nil
case msgDisconnect:
return m.handleDisconnect(packet)
case msgChannelOpen:
return m.handleChannelOpen(packet)
case msgGlobalRequest, msgRequestSuccess, msgRequestFailure:
return m.handleGlobalPacket(packet)
}
// assume a channel packet.
if len(packet) < 5 {
return parseError(packet[0])
}
id := binary.BigEndian.Uint32(packet[1:])
ch := m.chanList.getChan(id)
if ch == nil {
return fmt.Errorf("ssh: invalid channel %d", id)
}
return ch.handlePacket(packet)
}
func (m *mux) handleDisconnect(packet []byte) error {
var d disconnectMsg
if err := Unmarshal(packet, &d); err != nil {
return err
}
if debugMux {
log.Printf("caught disconnect: %v", d)
}
return &d
}
func (m *mux) handleGlobalPacket(packet []byte) error {
msg, err := decode(packet)
if err != nil {
return err
}
switch msg := msg.(type) {
case *globalRequestMsg:
m.incomingRequests <- &Request{
Type: msg.Type,
WantReply: msg.WantReply,
Payload: msg.Data,
mux: m,
}
case *globalRequestSuccessMsg, *globalRequestFailureMsg:
m.globalResponses <- msg
default:
panic(fmt.Sprintf("not a global message %#v", msg))
}
return nil
}
// handleChannelOpen schedules a channel to be Accept()ed.
func (m *mux) handleChannelOpen(packet []byte) error {
var msg channelOpenMsg
if err := Unmarshal(packet, &msg); err != nil {
return err
}
if msg.MaxPacketSize < minPacketLength || msg.MaxPacketSize > 1<<31 {
failMsg := channelOpenFailureMsg{
PeersId: msg.PeersId,
Reason: ConnectionFailed,
Message: "invalid request",
Language: "en_US.UTF-8",
}
return m.sendMessage(failMsg)
}
c := m.newChannel(msg.ChanType, channelInbound, msg.TypeSpecificData)
c.remoteId = msg.PeersId
c.maxRemotePayload = msg.MaxPacketSize
c.remoteWin.add(msg.PeersWindow)
m.incomingChannels <- c
return nil
}
func (m *mux) OpenChannel(chanType string, extra []byte) (Channel, <-chan *Request, error) {
ch, err := m.openChannel(chanType, extra)
if err != nil {
return nil, nil, err
}
return ch, ch.incomingRequests, nil
}
func (m *mux) openChannel(chanType string, extra []byte) (*channel, error) {
ch := m.newChannel(chanType, channelOutbound, extra)
ch.maxIncomingPayload = channelMaxPacket
open := channelOpenMsg{
ChanType: chanType,
PeersWindow: ch.myWindow,
MaxPacketSize: ch.maxIncomingPayload,
TypeSpecificData: extra,
PeersId: ch.localId,
}
if err := m.sendMessage(open); err != nil {
return nil, err
}
switch msg := (<-ch.msg).(type) {
case *channelOpenConfirmMsg:
return ch, nil
case *channelOpenFailureMsg:
return nil, &OpenChannelError{msg.Reason, msg.Message}
default:
return nil, fmt.Errorf("ssh: unexpected packet in response to channel open: %T", msg)
}
}

525
modules/crypto/ssh/mux_test.go

@ -1,525 +0,0 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"io"
"io/ioutil"
"sync"
"testing"
)
func muxPair() (*mux, *mux) {
a, b := memPipe()
s := newMux(a)
c := newMux(b)
return s, c
}
// Returns both ends of a channel, and the mux for the the 2nd
// channel.
func channelPair(t *testing.T) (*channel, *channel, *mux) {
c, s := muxPair()
res := make(chan *channel, 1)
go func() {
newCh, ok := <-s.incomingChannels
if !ok {
t.Fatalf("No incoming channel")
}
if newCh.ChannelType() != "chan" {
t.Fatalf("got type %q want chan", newCh.ChannelType())
}
ch, _, err := newCh.Accept()
if err != nil {
t.Fatalf("Accept %v", err)
}
res <- ch.(*channel)
}()
ch, err := c.openChannel("chan", nil)
if err != nil {
t.Fatalf("OpenChannel: %v", err)
}
return <-res, ch, c
}
// Test that stderr and stdout can be addressed from different
// goroutines. This is intended for use with the race detector.
func TestMuxChannelExtendedThreadSafety(t *testing.T) {
writer, reader, mux := channelPair(t)
defer writer.Close()
defer reader.Close()
defer mux.Close()
var wr, rd sync.WaitGroup
magic := "hello world"
wr.Add(2)
go func() {
io.WriteString(writer, magic)
wr.Done()
}()
go func() {
io.WriteString(writer.Stderr(), magic)
wr.Done()
}()
rd.Add(2)
go func() {
c, err := ioutil.ReadAll(reader)
if string(c) != magic {
t.Fatalf("stdout read got %q, want %q (error %s)", c, magic, err)
}
rd.Done()
}()
go func() {
c, err := ioutil.ReadAll(reader.Stderr())
if string(c) != magic {
t.Fatalf("stderr read got %q, want %q (error %s)", c, magic, err)
}
rd.Done()
}()
wr.Wait()
writer.CloseWrite()
rd.Wait()
}
func TestMuxReadWrite(t *testing.T) {
s, c, mux := channelPair(t)
defer s.Close()
defer c.Close()
defer mux.Close()
magic := "hello world"
magicExt := "hello stderr"
go func() {
_, err := s.Write([]byte(magic))
if err != nil {
t.Fatalf("Write: %v", err)
}
_, err = s.Extended(1).Write([]byte(magicExt))
if err != nil {
t.Fatalf("Write: %v", err)
}
err = s.Close()
if err != nil {
t.Fatalf("Close: %v", err)
}
}()
var buf [1024]byte
n, err := c.Read(buf[:])
if err != nil {
t.Fatalf("server Read: %v", err)
}
got := string(buf[:n])
if got != magic {
t.Fatalf("server: got %q want %q", got, magic)
}
n, err = c.Extended(1).Read(buf[:])
if err != nil {
t.Fatalf("server Read: %v", err)
}
got = string(buf[:n])
if got != magicExt {
t.Fatalf("server: got %q want %q", got, magic)
}
}
func TestMuxChannelOverflow(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
wDone := make(chan int, 1)
go func() {
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
writer.Write(make([]byte, 1))
wDone <- 1
}()
writer.remoteWin.waitWriterBlocked()
// Send 1 byte.
packet := make([]byte, 1+4+4+1)
packet[0] = msgChannelData
marshalUint32(packet[1:], writer.remoteId)
marshalUint32(packet[5:], uint32(1))
packet[9] = 42
if err := writer.mux.conn.writePacket(packet); err != nil {
t.Errorf("could not send packet")
}
if _, err := reader.SendRequest("hello", true, nil); err == nil {
t.Errorf("SendRequest succeeded.")
}
<-wDone
}
func TestMuxChannelCloseWriteUnblock(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
wDone := make(chan int, 1)
go func() {
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
if _, err := writer.Write(make([]byte, 1)); err != io.EOF {
t.Errorf("got %v, want EOF for unblock write", err)
}
wDone <- 1
}()
writer.remoteWin.waitWriterBlocked()
reader.Close()
<-wDone
}
func TestMuxConnectionCloseWriteUnblock(t *testing.T) {
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
wDone := make(chan int, 1)
go func() {
if _, err := writer.Write(make([]byte, channelWindowSize)); err != nil {
t.Errorf("could not fill window: %v", err)
}
if _, err := writer.Write(make([]byte, 1)); err != io.EOF {
t.Errorf("got %v, want EOF for unblock write", err)
}
wDone <- 1
}()
writer.remoteWin.waitWriterBlocked()
mux.Close()
<-wDone
}
func TestMuxReject(t *testing.T) {
client, server := muxPair()
defer server.Close()
defer client.Close()
go func() {
ch, ok := <-server.incomingChannels
if !ok {
t.Fatalf("Accept")
}
if ch.ChannelType() != "ch" || string(ch.ExtraData()) != "extra" {
t.Fatalf("unexpected channel: %q, %q", ch.ChannelType(), ch.ExtraData())
}
ch.Reject(RejectionReason(42), "message")
}()
ch, err := client.openChannel("ch", []byte("extra"))
if ch != nil {
t.Fatal("openChannel not rejected")
}
ocf, ok := err.(*OpenChannelError)
if !ok {
t.Errorf("got %#v want *OpenChannelError", err)
} else if ocf.Reason != 42 || ocf.Message != "message" {
t.Errorf("got %#v, want {Reason: 42, Message: %q}", ocf, "message")
}
want := "ssh: rejected: unknown reason 42 (message)"
if err.Error() != want {
t.Errorf("got %q, want %q", err.Error(), want)
}
}
func TestMuxChannelRequest(t *testing.T) {
client, server, mux := channelPair(t)
defer server.Close()
defer client.Close()
defer mux.Close()
var received int
var wg sync.WaitGroup
wg.Add(1)
go func() {
for r := range server.incomingRequests {
received++
r.Reply(r.Type == "yes", nil)
}
wg.Done()
}()
_, err := client.SendRequest("yes", false, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
ok, err := client.SendRequest("yes", true, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
if !ok {
t.Errorf("SendRequest(yes): %v", ok)
}
ok, err = client.SendRequest("no", true, nil)
if err != nil {
t.Fatalf("SendRequest: %v", err)
}
if ok {
t.Errorf("SendRequest(no): %v", ok)
}
client.Close()
wg.Wait()
if received != 3 {
t.Errorf("got %d requests, want %d", received, 3)
}
}
func TestMuxGlobalRequest(t *testing.T) {
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
var seen bool
go func() {
for r := range serverMux.incomingRequests {
seen = seen || r.Type == "peek"
if r.WantReply {
err := r.Reply(r.Type == "yes",
append([]byte(r.Type), r.Payload...))
if err != nil {
t.Errorf("AckRequest: %v", err)
}
}
}
}()
_, _, err := clientMux.SendRequest("peek", false, nil)
if err != nil {
t.Errorf("SendRequest: %v", err)
}
ok, data, err := clientMux.SendRequest("yes", true, []byte("a"))
if !ok || string(data) != "yesa" || err != nil {
t.Errorf("SendRequest(\"yes\", true, \"a\"): %v %v %v",
ok, data, err)
}
if ok, data, err := clientMux.SendRequest("yes", true, []byte("a")); !ok || string(data) != "yesa" || err != nil {
t.Errorf("SendRequest(\"yes\", true, \"a\"): %v %v %v",
ok, data, err)
}
if ok, data, err := clientMux.SendRequest("no", true, []byte("a")); ok || string(data) != "noa" || err != nil {
t.Errorf("SendRequest(\"no\", true, \"a\"): %v %v %v",
ok, data, err)
}
clientMux.Disconnect(0, "")
if !seen {
t.Errorf("never saw 'peek' request")
}
}
func TestMuxGlobalRequestUnblock(t *testing.T) {
clientMux, serverMux := muxPair()
defer serverMux.Close()
defer clientMux.Close()
result := make(chan error, 1)
go func() {
_, _, err := clientMux.SendRequest("hello", true, nil)
result <- err
}()
<-serverMux.incomingRequests
serverMux.conn.Close()
err := <-result
if err != io.EOF {
t.Errorf("want EOF, got %v", io.EOF)
}
}
func TestMuxChannelRequestUnblock(t *testing.T) {
a, b, connB := channelPair(t)
defer a.Close()
defer b.Close()
defer connB.Close()
result := make(chan error, 1)
go func() {
_, err := a.SendRequest("hello", true, nil)
result <- err
}()
<-b.incomingRequests
connB.conn.Close()
err := <-result
if err != io.EOF {
t.Errorf("want EOF, got %v", err)
}
}
func TestMuxDisconnect(t *testing.T) {
a, b := muxPair()
defer a.Close()
defer b.Close()
go func() {
for r := range b.incomingRequests {
r.Reply(true, nil)
}
}()
a.Disconnect(42, "whatever")
ok, _, err := a.SendRequest("hello", true, nil)
if ok || err == nil {
t.Errorf("got reply after disconnecting")
}
err = b.Wait()
if d, ok := err.(*disconnectMsg); !ok || d.Reason != 42 {
t.Errorf("got %#v, want disconnectMsg{Reason:42}", err)
}
}
func TestMuxCloseChannel(t *testing.T) {
r, w, mux := channelPair(t)
defer mux.Close()
defer r.Close()
defer w.Close()
result := make(chan error, 1)
go func() {
var b [1024]byte
_, err := r.Read(b[:])
result <- err
}()
if err := w.Close(); err != nil {
t.Errorf("w.Close: %v", err)
}
if _, err := w.Write([]byte("hello")); err != io.EOF {
t.Errorf("got err %v, want io.EOF after Close", err)
}
if err := <-result; err != io.EOF {
t.Errorf("got %v (%T), want io.EOF", err, err)
}
}
func TestMuxCloseWriteChannel(t *testing.T) {
r, w, mux := channelPair(t)
defer mux.Close()
result := make(chan error, 1)
go func() {
var b [1024]byte
_, err := r.Read(b[:])
result <- err
}()
if err := w.CloseWrite(); err != nil {
t.Errorf("w.CloseWrite: %v", err)
}
if _, err := w.Write([]byte("hello")); err != io.EOF {
t.Errorf("got err %v, want io.EOF after CloseWrite", err)
}
if err := <-result; err != io.EOF {
t.Errorf("got %v (%T), want io.EOF", err, err)
}
}
func TestMuxInvalidRecord(t *testing.T) {
a, b := muxPair()
defer a.Close()
defer b.Close()
packet := make([]byte, 1+4+4+1)
packet[0] = msgChannelData
marshalUint32(packet[1:], 29348723 /* invalid channel id */)
marshalUint32(packet[5:], 1)
packet[9] = 42
a.conn.writePacket(packet)
go a.SendRequest("hello", false, nil)
// 'a' wrote an invalid packet, so 'b' has exited.
req, ok := <-b.incomingRequests
if ok {
t.Errorf("got request %#v after receiving invalid packet", req)
}
}
func TestZeroWindowAdjust(t *testing.T) {
a, b, mux := channelPair(t)
defer a.Close()
defer b.Close()
defer mux.Close()
go func() {
io.WriteString(a, "hello")
// bogus adjust.
a.sendMessage(windowAdjustMsg{})
io.WriteString(a, "world")
a.Close()
}()
want := "helloworld"
c, _ := ioutil.ReadAll(b)
if string(c) != want {
t.Errorf("got %q want %q", c, want)
}
}
func TestMuxMaxPacketSize(t *testing.T) {
a, b, mux := channelPair(t)
defer a.Close()
defer b.Close()
defer mux.Close()
large := make([]byte, a.maxRemotePayload+1)
packet := make([]byte, 1+4+4+1+len(large))
packet[0] = msgChannelData
marshalUint32(packet[1:], a.remoteId)
marshalUint32(packet[5:], uint32(len(large)))
packet[9] = 42
if err := a.mux.conn.writePacket(packet); err != nil {
t.Errorf("could not send packet")
}
go a.SendRequest("hello", false, nil)
_, ok := <-b.incomingRequests
if ok {
t.Errorf("connection still alive after receiving large packet.")
}
}
// Don't ship code with debug=true.
func TestDebug(t *testing.T) {
if debugMux {
t.Error("mux debug switched on")
}
if debugHandshake {
t.Error("handshake debug switched on")
}
}

493
modules/crypto/ssh/server.go

@ -1,493 +0,0 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"bytes"
"errors"
"fmt"
"io"
"net"
)
// The Permissions type holds fine-grained permissions that are
// specific to a user or a specific authentication method for a
// user. Permissions, except for "source-address", must be enforced in
// the server application layer, after successful authentication. The
// Permissions are passed on in ServerConn so a server implementation
// can honor them.
type Permissions struct {
// Critical options restrict default permissions. Common
// restrictions are "source-address" and "force-command". If
// the server cannot enforce the restriction, or does not
// recognize it, the user should not authenticate.
CriticalOptions map[string]string
// Extensions are extra functionality that the server may
// offer on authenticated connections. Common extensions are
// "permit-agent-forwarding", "permit-X11-forwarding". Lack of
// support for an extension does not preclude authenticating a
// user.
Extensions map[string]string
}
// ServerConfig holds server specific configuration data.
type ServerConfig struct {
// Config contains configuration shared between client and server.
Config
hostKeys []Signer
// NoClientAuth is true if clients are allowed to connect without
// authenticating.
NoClientAuth bool
// PasswordCallback, if non-nil, is called when a user
// attempts to authenticate using a password.
PasswordCallback func(conn ConnMetadata, password []byte) (*Permissions, error)
// PublicKeyCallback, if non-nil, is called when a client attempts public
// key authentication. It must return true if the given public key is
// valid for the given user. For example, see CertChecker.Authenticate.
PublicKeyCallback func(conn ConnMetadata, key PublicKey) (*Permissions, error)
// KeyboardInteractiveCallback, if non-nil, is called when
// keyboard-interactive authentication is selected (RFC
// 4256). The client object's Challenge function should be
// used to query the user. The callback may offer multiple
// Challenge rounds. To avoid information leaks, the client
// should be presented a challenge even if the user is
// unknown.
KeyboardInteractiveCallback func(conn ConnMetadata, client KeyboardInteractiveChallenge) (*Permissions, error)
// AuthLogCallback, if non-nil, is called to log all authentication
// attempts.
AuthLogCallback func(conn ConnMetadata, method string, err error)
// ServerVersion is the version identification string to
// announce in the public handshake.
// If empty, a reasonable default is used.
ServerVersion string
}
// AddHostKey adds a private key as a host key. If an existing host
// key exists with the same algorithm, it is overwritten. Each server
// config must have at least one host key.
func (s *ServerConfig) AddHostKey(key Signer) {
for i, k := range s.hostKeys {
if k.PublicKey().Type() == key.PublicKey().Type() {
s.hostKeys[i] = key
return
}
}
s.hostKeys = append(s.hostKeys, key)
}
// cachedPubKey contains the results of querying whether a public key is
// acceptable for a user.
type cachedPubKey struct {
user string
pubKeyData []byte
result error
perms *Permissions
}
const maxCachedPubKeys = 16
// pubKeyCache caches tests for public keys. Since SSH clients
// will query whether a public key is acceptable before attempting to
// authenticate with it, we end up with duplicate queries for public
// key validity. The cache only applies to a single ServerConn.
type pubKeyCache struct {
keys []cachedPubKey
}
// get returns the result for a given user/algo/key tuple.
func (c *pubKeyCache) get(user string, pubKeyData []byte) (cachedPubKey, bool) {
for _, k := range c.keys {
if k.user == user && bytes.Equal(k.pubKeyData, pubKeyData) {
return k, true
}
}
return cachedPubKey{}, false
}
// add adds the given tuple to the cache.
func (c *pubKeyCache) add(candidate cachedPubKey) {
if len(c.keys) < maxCachedPubKeys {
c.keys = append(c.keys, candidate)
}
}
// ServerConn is an authenticated SSH connection, as seen from the
// server
type ServerConn struct {
Conn
// If the succeeding authentication callback returned a
// non-nil Permissions pointer, it is stored here.
Permissions *Permissions
}
// NewServerConn starts a new SSH server with c as the underlying
// transport. It starts with a handshake and, if the handshake is
// unsuccessful, it closes the connection and returns an error. The
// Request and NewChannel channels must be serviced, or the connection
// will hang.
func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewChannel, <-chan *Request, error) {
fullConf := *config
fullConf.SetDefaults()
s := &connection{
sshConn: sshConn{conn: c},
}
perms, err := s.serverHandshake(&fullConf)
if err != nil {
c.Close()
return nil, nil, nil, err
}
return &ServerConn{s, perms}, s.mux.incomingChannels, s.mux.incomingRequests, nil
}
// signAndMarshal signs the data with the appropriate algorithm,
// and serializes the result in SSH wire format.
func signAndMarshal(k Signer, rand io.Reader, data []byte) ([]byte, error) {
sig, err := k.Sign(rand, data)
if err != nil {
return nil, err
}
return Marshal(sig), nil
}
// handshake performs key exchange and user authentication.
func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error) {
if len(config.hostKeys) == 0 {
return nil, errors.New("ssh: server has no host keys")
}
if !config.NoClientAuth && config.PasswordCallback == nil && config.PublicKeyCallback == nil && config.KeyboardInteractiveCallback == nil {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
if config.ServerVersion != "" {
s.serverVersion = []byte(config.ServerVersion)
} else {
s.serverVersion = []byte(packageVersion)
}
var err error
s.clientVersion, err = exchangeVersions(s.sshConn.conn, s.serverVersion)
if err != nil {
return nil, err
}
tr := newTransport(s.sshConn.conn, config.Rand, false /* not client */)
s.transport = newServerTransport(tr, s.clientVersion, s.serverVersion, config)
if err := s.transport.requestKeyChange(); err != nil {
return nil, err
}
if packet, err := s.transport.readPacket(); err != nil {
return nil, err
} else if packet[0] != msgNewKeys {
return nil, unexpectedMessageError(msgNewKeys, packet[0])
}
// We just did the key change, so the session ID is established.
s.sessionID = s.transport.getSessionID()
var packet []byte
if packet, err = s.transport.readPacket(); err != nil {
return nil, err
}
var serviceRequest serviceRequestMsg
if err = Unmarshal(packet, &serviceRequest); err != nil {
return nil, err
}
if serviceRequest.Service != serviceUserAuth {
return nil, errors.New("ssh: requested service '" + serviceRequest.Service + "' before authenticating")
}
serviceAccept := serviceAcceptMsg{
Service: serviceUserAuth,
}
if err := s.transport.writePacket(Marshal(&serviceAccept)); err != nil {
return nil, err
}
perms, err := s.serverAuthenticate(config)
if err != nil {
return nil, err
}
s.mux = newMux(s.transport)
return perms, err
}
func isAcceptableAlgo(algo string) bool {
switch algo {
case KeyAlgoRSA, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521,
CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01:
return true
}
return false
}
func checkSourceAddress(addr net.Addr, sourceAddr string) error {
if addr == nil {
return errors.New("ssh: no address known for client, but source-address match required")
}
tcpAddr, ok := addr.(*net.TCPAddr)
if !ok {
return fmt.Errorf("ssh: remote address %v is not an TCP address when checking source-address match", addr)
}
if allowedIP := net.ParseIP(sourceAddr); allowedIP != nil {
if bytes.Equal(allowedIP, tcpAddr.IP) {
return nil
}
} else {
_, ipNet, err := net.ParseCIDR(sourceAddr)
if err != nil {
return fmt.Errorf("ssh: error parsing source-address restriction %q: %v", sourceAddr, err)
}
if ipNet.Contains(tcpAddr.IP) {
return nil
}
}
return fmt.Errorf("ssh: remote address %v is not allowed because of source-address restriction", addr)
}
func (s *connection) serverAuthenticate(config *ServerConfig) (*Permissions, error) {
var err error
var cache pubKeyCache
var perms *Permissions
userAuthLoop:
for {
var userAuthReq userAuthRequestMsg
if packet, err := s.transport.readPacket(); err != nil {
return nil, err
} else if err = Unmarshal(packet, &userAuthReq); err != nil {
return nil, err
}
if userAuthReq.Service != serviceSSH {
return nil, errors.New("ssh: client attempted to negotiate for unknown service: " + userAuthReq.Service)
}
s.user = userAuthReq.User
perms = nil
authErr := errors.New("no auth passed yet")
switch userAuthReq.Method {
case "none":
if config.NoClientAuth {
s.user = ""
authErr = nil
}
case "password":
if config.PasswordCallback == nil {
authErr = errors.New("ssh: password auth not configured")
break
}
payload := userAuthReq.Payload
if len(payload) < 1 || payload[0] != 0 {
return nil, parseError(msgUserAuthRequest)
}
payload = payload[1:]
password, payload, ok := parseString(payload)
if !ok || len(payload) > 0 {
return nil, parseError(msgUserAuthRequest)
}
perms, authErr = config.PasswordCallback(s, password)
case "keyboard-interactive":
if config.KeyboardInteractiveCallback == nil {
authErr = errors.New("ssh: keyboard-interactive auth not configubred")
break
}
prompter := &sshClientKeyboardInteractive{s}
perms, authErr = config.KeyboardInteractiveCallback(s, prompter.Challenge)
case "publickey":
if config.PublicKeyCallback == nil {
authErr = errors.New("ssh: publickey auth not configured")
break
}
payload := userAuthReq.Payload
if len(payload) < 1 {
return nil, parseError(msgUserAuthRequest)
}
isQuery := payload[0] == 0
payload = payload[1:]
algoBytes, payload, ok := parseString(payload)
if !ok {
return nil, parseError(msgUserAuthRequest)
}
algo := string(algoBytes)
if !isAcceptableAlgo(algo) {
authErr = fmt.Errorf("ssh: algorithm %q not accepted", algo)
break
}
pubKeyData, payload, ok := parseString(payload)
if !ok {
return nil, parseError(msgUserAuthRequest)
}
pubKey, err := ParsePublicKey(pubKeyData)
if err != nil {
return nil, err
}
candidate, ok := cache.get(s.user, pubKeyData)
if !ok {
candidate.user = s.user
candidate.pubKeyData = pubKeyData
candidate.perms, candidate.result = config.PublicKeyCallback(s, pubKey)
if candidate.result == nil && candidate.perms != nil && candidate.perms.CriticalOptions != nil && candidate.perms.CriticalOptions[sourceAddressCriticalOption] != "" {
candidate.result = checkSourceAddress(
s.RemoteAddr(),
candidate.perms.CriticalOptions[sourceAddressCriticalOption])
}
cache.add(candidate)
}
if isQuery {
// The client can query if the given public key
// would be okay.
if len(payload) > 0 {
return nil, parseError(msgUserAuthRequest)
}
if candidate.result == nil {
okMsg := userAuthPubKeyOkMsg{
Algo: algo,
PubKey: pubKeyData,
}
if err = s.transport.writePacket(Marshal(&okMsg)); err != nil {
return nil, err
}
continue userAuthLoop
}
authErr = candidate.result
} else {
sig, payload, ok := parseSignature(payload)
if !ok || len(payload) > 0 {
return nil, parseError(msgUserAuthRequest)
}
// Ensure the public key algo and signature algo
// are supported. Compare the private key
// algorithm name that corresponds to algo with
// sig.Format. This is usually the same, but
// for certs, the names differ.
if !isAcceptableAlgo(sig.Format) {
break
}
signedData := buildDataSignedForAuth(s.transport.getSessionID(), userAuthReq, algoBytes, pubKeyData)
if err := pubKey.Verify(signedData, sig); err != nil {
return nil, err
}
authErr = candidate.result
perms = candidate.perms
}
default:
authErr = fmt.Errorf("ssh: unknown method %q", userAuthReq.Method)
}
if config.AuthLogCallback != nil {
config.AuthLogCallback(s, userAuthReq.Method, authErr)
}
if authErr == nil {
break userAuthLoop
}
var failureMsg userAuthFailureMsg
if config.PasswordCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "password")
}
if config.PublicKeyCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "publickey")
}
if config.KeyboardInteractiveCallback != nil {
failureMsg.Methods = append(failureMsg.Methods, "keyboard-interactive")
}
if len(failureMsg.Methods) == 0 {
return nil, errors.New("ssh: no authentication methods configured but NoClientAuth is also false")
}
if err = s.transport.writePacket(Marshal(&failureMsg)); err != nil {
return nil, err
}
}
if err = s.transport.writePacket([]byte{msgUserAuthSuccess}); err != nil {
return nil, err
}
return perms, nil
}
// sshClientKeyboardInteractive implements a ClientKeyboardInteractive by
// asking the client on the other side of a ServerConn.
type sshClientKeyboardInteractive struct {
*connection
}
func (c *sshClientKeyboardInteractive) Challenge(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
if len(questions) != len(echos) {
return nil, errors.New("ssh: echos and questions must have equal length")
}
var prompts []byte
for i := range questions {
prompts = appendString(prompts, questions[i])
prompts = appendBool(prompts, echos[i])
}
if err := c.transport.writePacket(Marshal(&userAuthInfoRequestMsg{
Instruction: instruction,
NumPrompts: uint32(len(questions)),
Prompts: prompts,
})); err != nil {
return nil, err
}
packet, err := c.transport.readPacket()
if err != nil {
return nil, err
}
if packet[0] != msgUserAuthInfoResponse {
return nil, unexpectedMessageError(msgUserAuthInfoResponse, packet[0])
}
packet = packet[1:]
n, packet, ok := parseUint32(packet)
if !ok || int(n) != len(questions) {
return nil, parseError(msgUserAuthInfoResponse)
}
for i := uint32(0); i < n; i++ {
ans, rest, ok := parseString(packet)
if !ok {
return nil, parseError(msgUserAuthInfoResponse)
}
answers = append(answers, string(ans))
packet = rest
}
if len(packet) != 0 {
return nil, errors.New("ssh: junk at end of message")
}
return answers, nil
}

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

Loading…
Cancel
Save