Compare commits

...

71 Commits

Author SHA1 Message Date
无闻
9e61ec316e Merge pull request #830 from TonyTsangHK/dev
Add max-width to commit message of repo-files-table
2015-01-05 20:45:06 +08:00
Unknwon
13e35398aa prepare for mirror update release 2015-01-05 20:37:22 +08:00
Tony Tsang
9f4e584122 Add max-width to commit message of repo-files-table 2015-01-05 12:22:20 +08:00
Unknwon
d5c6b53571 use new Redis client 2015-01-03 02:54:22 +08:00
Unknwon
f0ca16d78f add cache version require 2015-01-02 23:59:06 +08:00
无闻
9b0858b1ad Merge pull request #823 from phsmit/fix_799
Fix #799 by adding a tooltip for all dates in all settings/panels
2015-01-02 20:25:48 +08:00
Unknwon
c73e9057ae Optmize git-fsck options and fix #820 2015-01-02 20:14:43 +08:00
Peter Smit
fd70f9ec1b Fix #799 by adding a tooltip for all dates in all settings/panels 2015-01-02 12:41:05 +02:00
Unknwon
0b56272c13 fix #805 2015-01-02 13:30:45 +08:00
Unknwon
677b1ec627 fix #808 2015-01-02 12:56:08 +08:00
Unknwon
3ea1443885 work on #784 2015-01-02 12:47:33 +08:00
Unknwon
b6272d1803 fix gopmfile 2014-12-31 21:54:25 +08:00
Unknwon
40de2f78c4 fix gopmfile 2014-12-31 18:41:34 +08:00
Unknwon
aa2148a7a9 fix gopmfile 2014-12-31 18:39:47 +08:00
Unknwon
bd555551ce fix #801 2014-12-31 18:37:29 +08:00
Unknwon
e1c5008238 Merge branch 'dev' of github.com:gogits/gogs into dev 2014-12-31 17:09:07 +08:00
Unknwon
0f1ff9b1ad fix cache dep API broken 2014-12-31 17:08:57 +08:00
无闻
52cc58fd9d Merge pull request #797 from Mikayex/dev
Fix #795
2014-12-29 20:09:50 +08:00
无闻
477bea574a Merge pull request #782 from estetsenko/dev
fix for  #747
2014-12-29 20:08:39 +08:00
无闻
37566f71a9 Merge pull request #787 from andre-hub/dev
flexible the build scripts and add a freebsd build script
2014-12-29 20:07:10 +08:00
无闻
afdb0c7f9d Merge pull request #790 from euank/master
Initialize unset git user.email / user.name correctly; fix Dockerfile
2014-12-29 20:06:27 +08:00
Unknwon
63c1f9a23f fix 500: E-mail already used in user settings page 2014-12-29 20:00:07 +08:00
Thomas Laroche
e948c7c262 Fix #795 2014-12-29 11:55:46 +01:00
Unknwon
81a44e4cd0 fix API changes 2014-12-28 20:40:35 +08:00
euank
f059866a21 Set user.name & user.email in Dockerfile
The previous setting would cause all repo creations to fail, as
described in issue #328.
The previous commit also resolves this issue, but it seems saner to
create the user in the Dockerfile than at runtime.
2014-12-27 19:10:33 -08:00
euank
234a7c19a4 Default values for both user.name and user.email
The previous behavior was to set default values only if user.name was
not set, but to always set it for both. This only sets a value if there
wasn't one; this fixes cases where someone has a user.name but no
user.email (see included Dockerfile) or someone has a user.email but no
user.name (before the email would have been over-written).
2014-12-27 19:07:54 -08:00
André Grötschel
35dd41c3a2 merge flexible the build scripts and add a freebsd build script 2014-12-27 15:28:45 +01:00
André Grötschel
afc659442d flexiable the build scripts and add a freebsd build script 2014-12-27 15:16:05 +01:00
Unknwon
6a6636d451 Merge branch 'master' of github.com:gogits/gogs into dev 2014-12-27 12:38:18 +08:00
无闻
46742a79ca Merge pull request #785 from joshk/patch-1
Use the new build env on Travis
2014-12-27 12:25:08 +08:00
无闻
d61def86e1 Merge pull request #780 from morpheyesh/master
spellcheck
2014-12-27 12:24:21 +08:00
Josh Kalderimis
30c750a5df Use the new build env on Travis
http://docs.travis-ci.com/user/workers/container-based-infrastructure
2014-12-27 01:23:10 +01:00
estetsenko
bae1d65564 bugfix: Unable to assign any issue myself 2014-12-24 20:47:45 +03:00
morpheyesh
995a805a31 spellcheck 2014-12-24 13:20:10 +05:30
fuxiaohei
c0cfd62b90 add label-edit and label-delete logic 2014-12-22 21:26:05 +08:00
Unknwon
ebbe6177a9 Merge branch 'dev' of github.com:gogits/gogs into dev 2014-12-22 03:44:52 -05:00
Unknwon
97b39ae2e4 fix invite bug(shouldn't include full name to search box) 2014-12-22 03:44:49 -05:00
无闻
0f77ad219c Merge pull request #771 from phsmit/multiple_email_cleanup
Create English locale keys for multiple e-mails feature
2014-12-22 02:43:20 -05:00
Peter Smit
8b31be43c6 Forgot to i18n "Primary" 2014-12-22 09:41:29 +02:00
Peter Smit
21dbcb7c77 Create English locale keys for multiple e-mails feature
Also, change all current 'emails' to 'e-mails'.
Still todo: some CSS for the user/settings/email page, but that is not my specialty
2014-12-22 09:11:30 +02:00
无闻
0d7bb9af46 Merge pull request #769 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2014-12-21 02:58:38 -05:00
The Gitter Badger
86e9ebdcc9 Added Gitter badge 2014-12-21 07:53:06 +00:00
无闻
350e0080e6 Merge pull request #767 from phsmit/fix_email_query
Fix for wrong email query
2014-12-21 02:23:36 -05:00
Peter Smit
66e2016eeb Fix for wrong email query
Changing EmailAdress.OwnerId to EmailAddress.Uid should have accompanied this change
2014-12-21 09:16:56 +02:00
无闻
030b3d751e Merge pull request #766 from phsmit/correct_from
Correct usage of FROM in email creation
2014-12-21 01:55:28 -05:00
Unknwon
79537467da mirror fix and update 2014-12-20 22:51:16 -05:00
无闻
a18decf4cc Merge pull request #755 from phsmit/multiple_emails
Multiple emails
2014-12-20 22:47:05 -05:00
Peter
20b5c23a19 Small fixes to multiple_emails feature 2014-12-20 09:26:51 +02:00
无闻
e6e2cf7855 Merge pull request #764 from phsmit/fix_mailer
Remove standard mailer port lines
2014-12-19 17:20:13 -05:00
Peter
c884ecfea1 Parse the from string to extract the email address 2014-12-19 23:06:03 +02:00
Peter
edbe1de026 Remove unused "User" member of Message Struct and fix bounce address
The User member of a message is not needed anymore.

The from that is send to the server, should always be the "system" from. This is also called the Bounce address http://en.wikipedia.org/wiki/Bounce_address
2014-12-19 22:48:21 +02:00
Peter
2321b4b272 Change from header in email, fixes #765 2014-12-19 22:33:17 +02:00
Peter
007cf33e88 Remove standard mailer port lines
This lines got committed by accident. They do actually nothing, as SplitHostPort will give an error if port is not given.
2014-12-19 22:00:11 +02:00
Unknwon
b231b8c927 update locale and mirror code format 2014-12-19 00:24:17 -05:00
无闻
d01e7b0173 Merge pull request #762 from phsmit/crammd5
Crammd5
2014-12-18 16:49:11 -05:00
无闻
bb267e30b6 Merge pull request #761 from phsmit/mailer_rewritten
Rewrite of SendMail function
2014-12-18 16:37:24 -05:00
Peter
eca42bcb44 Prefer CRAM-MD5 over PLAIN authentication 2014-12-18 14:15:13 +02:00
Peter
5ffeca35e7 Add option to use CRAM-MD5 as authentication method in the mailer 2014-12-18 13:58:48 +02:00
Peter
87be137b88 Rewrite of SendMail function
The SendMail function is rewritten and has the following new functionality:
 - It is optional to skip verification of keys. The config option SKIP_VERIFY is added
 - If the port is 465, or ending on 465, the TLS(/SSL) connection is started first.
2014-12-18 13:34:30 +02:00
Unknwon
0a697517ac work on #754 2014-12-18 03:37:31 -05:00
Unknwon
c4820f119d work on #754 2014-12-18 03:26:09 -05:00
Unknwon
57b3be4016 work on #756 2014-12-17 23:04:05 -05:00
Peter
b033f2f535 Finish method for having multiple emails/user.
All basics are implemented. Missing are the right (localized) strings
and the page markup could have a look at by a frontend guy.
2014-12-17 17:42:54 +02:00
Peter
f34b04cfc0 Template for email activation email 2014-12-17 17:42:31 +02:00
Peter
ec71d538fc Method for activating email addresses through verification email 2014-12-17 17:41:49 +02:00
Peter
6919c80f0b Add function to the model for email address management (add/delete/activate) 2014-12-17 17:40:10 +02:00
Peter Smit
99599c099f Add alternative email addresses to the model
A new struct is created named EmailAddress that contains alternative
email addresses for users. Also the email related methods; IsEmailUsed
and GetUserByEmail are updated.

DeleteUser deletes the extra email addresses and DeleteInactivateUsers
also deletes inactive accounts. This could be factored out, but should
do it for now.
2014-12-17 10:26:19 +02:00
Unknwon
d01f2f3c22 fix #751 2014-12-16 20:47:10 -05:00
Unknwon
1a5aa5e0c0 fix #741 2014-12-16 02:28:57 -05:00
Unknwon
9803c421f5 fix binding api broken 2014-12-15 01:49:59 -05:00
Unknwon
792ec63c8a update locale 2014-12-14 22:47:09 -05:00
56 changed files with 1111 additions and 431 deletions

View File

@@ -3,31 +3,31 @@ path = github.com/gogits/gogs
[deps]
github.com/beego/memcache = commit:2aea774416
github.com/beego/redigo = commit:856744a0d5
github.com/Unknwon/cae = commit:2e70a1351b
github.com/Unknwon/com = commit:2cbcbc6916
github.com/Unknwon/goconfig = commit:0f8d8dc1c0
github.com/Unknwon/i18n = commit:aec5f77857
github.com/Unknwon/macaron = commit:3d307c1232
github.com/codegangsta/cli = commit:7381bc4e62
github.com/go-sql-driver/mysql = commit:8111ee3ec3
github.com/go-xorm/core = commit:3e0fa232ab
github.com/go-xorm/xorm = commit:58d33844ce
github.com/Unknwon/com = commit:d9bcf409c8
github.com/Unknwon/i18n = commit:1e88666229
github.com/Unknwon/macaron = commit:256ba0beab
github.com/codegangsta/cli = commit:a14c5b47c7
github.com/go-sql-driver/mysql = commit:04cf947760
github.com/go-xorm/core = commit:e7882d8b00
github.com/go-xorm/xorm = commit:dcc529b68a
github.com/gogits/go-gogs-client = commit:92e76d616a
github.com/gogits/oauth2 = commit:99cbec870a
github.com/lib/pq = commit:b021d0ef20
github.com/macaron-contrib/binding = commit:0e23661e7d
github.com/macaron-contrib/cache = commit:0bb9e6c9ef
github.com/lib/pq = commit:3e3efe51a0
github.com/macaron-contrib/binding = commit:0fbe4b9707
github.com/macaron-contrib/cache = commit:1d99a5b621
github.com/macaron-contrib/captcha = commit:3567dc48b8
github.com/macaron-contrib/csrf = commit:422b79675c
github.com/macaron-contrib/i18n = commit:184bb832f2
github.com/macaron-contrib/csrf = commit:3ea14e7ee7
github.com/macaron-contrib/i18n = commit:0ee0539c84
github.com/macaron-contrib/oauth2 = commit:8f394c3629
github.com/macaron-contrib/session = commit:f00d48fd4f
github.com/macaron-contrib/session = commit:2633cef14f
github.com/macaron-contrib/toolbox = commit:57127bcc89
github.com/mattn/go-sqlite3 = commit:a80c27ba33
github.com/nfnt/resize = commit:581d15cb53
github.com/nfnt/resize = commit:8f44931448
github.com/russross/blackfriday = commit:05b8cefd6a
github.com/shurcooL/go = commit:48293cbc7a
github.com/saintfish/chardet = commit:3af4cd4741
gopkg.in/ini.v1 = commit:28ad8c408b
gopkg.in/redis.v2 = commit:e617904962
[res]
include = conf|etc|public|scripts|templates

View File

@@ -2,4 +2,6 @@ language: go
go:
- 1.2
- 1.3
- 1.3
sudo: false

View File

@@ -64,8 +64,17 @@ Before open any new issue, please check your problem on [Troubleshooting](http:/
Please take a moment to check that an issue on [GitHub](https://github.com/gogits/gogs/issues) or card on [Trello](https://trello.com/b/uxAoeLUl/gogs-go-git-service) doesn't already exist documenting your bug report or improvement proposal. If it does, it never hurts to add a quick "+1" or "I have this problem too". This will help prioritize the most common problems and requests.
### Discuss your design on the mailing list
## Code of conduct
We recommend discussing your plans [on the mailing list](https://groups.google.com/forum/#!forum/gogits) before starting to code - especially for more ambitious contributions. This gives other contributors a chance to point you in the right direction, give feedback on your design, and maybe point out if someone else is working on the same thing.
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We may close your pull request if not first discussed on the mailing list. We aren't doing this to be jerks. We are doing this to prevent people from spending large amounts of time on changes that may need to be designed or architected in a specific way, or may not align with the vision of the project.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior can be
reported by emailing contact@gitlab.com
This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)

View File

@@ -1,11 +1,13 @@
Gogs - Go Git Service [![wercker status](https://app.wercker.com/status/ad0bdb0bc450ac6f09bc56b9640a50aa/s/ "wercker status")](https://app.wercker.com/project/bykey/ad0bdb0bc450ac6f09bc56b9640a50aa) [![Build Status](https://travis-ci.org/gogits/gogs.svg?branch=master)](https://travis-ci.org/gogits/gogs)
=====================
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gogits/gogs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Gogs(Go Git Service) is a painless self-hosted Git Service written in Go.
![Demo](https://gowalker.org/public/gogs_demo.gif)
##### Current version: 0.5.8 Beta
##### Current version: 0.5.11 Beta
### NOTICES

View File

@@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个基于 Go 语言的自助 Git 服务。
![Demo](https://gowalker.org/public/gogs_demo.gif)
##### 当前版本0.5.8 Beta
##### 当前版本0.5.11 Beta
## 开发目的

View File

@@ -25,6 +25,7 @@ import (
"github.com/macaron-contrib/oauth2"
"github.com/macaron-contrib/session"
"github.com/macaron-contrib/toolbox"
"gopkg.in/ini.v1"
api "github.com/gogits/go-gogs-client"
@@ -55,6 +56,12 @@ and it takes care of all the other things for you`,
Flags: []cli.Flag{},
}
type VerChecker struct {
ImportPath string
Version func() string
Expected string
}
// checkVersion checks if binary matches the version of templates files.
func checkVersion() {
// Templates.
@@ -67,17 +74,20 @@ func checkVersion() {
}
// Check dependency version.
macaronVer := git.MustParseVersion(strings.Join(strings.Split(macaron.Version(), ".")[:3], "."))
if macaronVer.LessThan(git.MustParseVersion("0.4.7")) {
log.Fatal(4, "Package macaron version is too old, did you forget to update?(github.com/Unknwon/macaron)")
checkers := []VerChecker{
{"github.com/Unknwon/macaron", macaron.Version, "0.4.9"},
{"github.com/macaron-contrib/binding", binding.Version, "0.0.4"},
{"github.com/macaron-contrib/cache", cache.Version, "0.0.5"},
{"github.com/macaron-contrib/csrf", csrf.Version, "0.0.1"},
{"github.com/macaron-contrib/i18n", i18n.Version, "0.0.5"},
{"github.com/macaron-contrib/session", session.Version, "0.1.4"},
{"gopkg.in/ini.v1", ini.Version, "1.0.1"},
}
i18nVer := git.MustParseVersion(i18n.Version())
if i18nVer.LessThan(git.MustParseVersion("0.0.2")) {
log.Fatal(4, "Package i18n version is too old, did you forget to update?(github.com/macaron-contrib/i18n)")
}
sessionVer := git.MustParseVersion(session.Version())
if sessionVer.LessThan(git.MustParseVersion("0.0.5")) {
log.Fatal(4, "Package session version is too old, did you forget to update?(github.com/macaron-contrib/session)")
for _, c := range checkers {
ver := strings.Join(strings.Split(c.Version(), ".")[:3], ".")
if git.MustParseVersion(ver).LessThan(git.MustParseVersion(c.Expected)) {
log.Fatal(4, "Package '%s' version is too old(%s -> %s), did you forget to update?", c.ImportPath, ver, c.Expected)
}
}
}
@@ -119,18 +129,15 @@ func newMacaron() *macaron.Macaron {
Redirect: true,
}))
m.Use(cache.Cacher(cache.Options{
Adapter: setting.CacheAdapter,
Interval: setting.CacheInternal,
Conn: setting.CacheConn,
Adapter: setting.CacheAdapter,
AdapterConfig: setting.CacheConn,
Interval: setting.CacheInternal,
}))
m.Use(captcha.Captchaer(captcha.Options{
SubURL: setting.AppSubUrl,
}))
m.Use(session.Sessioner(session.Options{
Provider: setting.SessionProvider,
Config: *setting.SessionConfig,
}))
m.Use(csrf.Generate(csrf.Options{
m.Use(session.Sessioner(setting.SessionConfig))
m.Use(csrf.Csrfer(csrf.Options{
Secret: setting.SecretKey,
SetCookie: true,
Header: "X-Csrf-Token",
@@ -235,6 +242,8 @@ func runWeb(*cli.Context) {
m.Get("", user.Settings)
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost)
m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), user.SettingsAvatar)
m.Get("/email", user.SettingsEmails)
m.Post("/email", bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost)
m.Get("/password", user.SettingsPassword)
m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost)
m.Get("/ssh", user.SettingsSSHKeys)
@@ -246,6 +255,7 @@ func runWeb(*cli.Context) {
m.Group("/user", func() {
// r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds)
m.Any("/activate", user.Activate)
m.Any("/activate_email", user.ActivateEmail)
m.Get("/email2user", user.Email2User)
m.Get("/forget_password", user.ForgotPasswd)
m.Post("/forget_password", user.ForgotPasswdPost)

View File

@@ -94,8 +94,11 @@ SUBJECT = %(APP_NAME)s
; Mail server
; Gmail: smtp.gmail.com:587
; QQ: smtp.qq.com:25
; Note, if the port ends with "465", SMTPS will be used. Using STARTTLS on port 587 is recommended per RFC 6409. If the server supports STARTTLS it will always be used.
HOST =
; Mail from address
; Do not verify the certificate of the server. Only use this for self-signed certificates
SKIP_VERIFY =
; Mail from address, RFC 5322. This can be just an email address, or the "Name" <email@example.com> format
FROM =
; Mailer user name and password
USER =
@@ -151,7 +154,7 @@ HOST =
[session]
; Either "memory", "file", "redis" or "mysql", default is "memory"
PROVIDER = file
PROVIDER = memory
; Provider config options
; memory: not have any config yet
; file: session file path, e.g. `data/sessions`
@@ -257,14 +260,20 @@ DRIVER =
CONN =
[git]
MAX_GITDIFF_LINES = 10000
; Arguments for command 'git fsck', e.g.: "--unreachable --tags"
; see more on http://git-scm.com/docs/git-fsck/1.7.5
FSCK_ARGS =
MAX_GIT_DIFF_LINES = 10000
; Arguments for command 'git gc', e.g.: "--aggressive --auto"
; see more on http://git-scm.com/docs/git-gc/1.7.5
GC_ARGS =
; Git health check.
[git.fsck]
ENABLE = true
; Execution interval in hours. Default is 24.
INTERVAL = 24
; Arguments for command 'git fsck', e.g.: "--unreachable --tags"
; see more on http://git-scm.com/docs/git-fsck/1.7.5
ARGS =
[i18n]
LANGS = en-US,zh-CN,zh-HK,de-DE,fr-CA,nl-NL,lv-LV
NAMES = English,简体中文,繁體中文,Deutsch,Français,Nederlands,Latviešu

View File

@@ -61,7 +61,7 @@ domain = Domain
domain_helper = This affects SSH clone URLs.
app_url = Application URL
app_url_helper = This affects HTTP/HTTPS clone URL and somewhere in e-mail.
email_title = Email Service Settings(Optional)
email_title = E-mail Service Settings(Optional)
smtp_host = SMTP Host
mailer_user = Sender E-mail
mailer_password = Sender Password
@@ -109,7 +109,7 @@ confirmation_mail_sent_prompt = A new confirmation e-mail has been sent to <b>%s
sign_in_email = Sign in to your e-mail
active_your_account = Activate Your Account
resent_limit_prompt = Sorry, you are sending an activation e-mail too frequently. Please wait 3 minutes.
has_unconfirmed_mail = Hi %s, you have an unconfirmed email 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.
resend_mail = Click here to resend your activation e-mail
email_not_associate = This e-mail address does not associate to any account.
send_reset_mail = Click here to (re)send your password reset e-mail
@@ -192,7 +192,7 @@ delete = Delete Account
uid = Uid
public_profile = Public Profile
profile_desc = Your Email address is public and will be used for any account related notifications, and any web based operations made via the site.
profile_desc = Your E-mail address is public and will be used for any account related notifications, and any web based operations made via the site.
full_name = Full Name
website = Website
location = Location
@@ -217,6 +217,15 @@ new_password = New Password
password_incorrect = Current password is not correct.
change_password_success = Password is changed successfully. You can now sign in via new password.
emails = E-mail Addresses
manage_emails = Manage e-mail addresses
email_desc = Current e-mail addresses
primary = Primary
primary_email = Make primary
delete_email = Delete
add_new_email = Add new e-mail address
add_email = Add e-mail
manage_ssh_keys = Manage SSH Keys
add_key = Add Key
ssh_desc = This is a list of SSH keys associated with your account. Remove any keys that you do not recognize.
@@ -404,7 +413,7 @@ release.tag_name_already_exist = Release with this tag name has already existed.
[org]
org_name_holder = Organization Name
org_name_helper = Great organization names are short and memorable.
org_email_helper = Organization's Email receives all notifications and confirmations.
org_email_helper = Organization's E-mail receives all notifications and confirmations.
create_org = Create Organization
repo_updated = Updated
people = People
@@ -619,7 +628,7 @@ config.db_ssl_mode_helper = (for "postgres" only)
config.db_path = Path
config.db_path_helper = (for "sqlite3" only)
config.service_config = Service Configuration
config.register_email_confirm = Register Email Confirmation
config.register_email_confirm = Register E-mail Confirmation
config.disable_register = Disable Registration
config.require_sign_in_view = Require Sign In View
config.mail_notify = Mail Notification

View File

@@ -26,7 +26,7 @@ organization=Organisation
mirror=Miroir
new_repo=Nouveau Référentiel
new_migrate=Nouvelle Migration
new_fork=Nouveau référentiel de fourche
new_fork=Nouveau Référentiel d'Embranchement
new_org=Nouvelle Organisation
manage_org=Gérer les Organisations
admin_panel=Administration
@@ -257,9 +257,9 @@ repo_name=Nom du Référentiel
repo_name_helper=Idéalement, le nom d'un Référentiel devrait être court, mémorable et <strong>unique</strong>.
visibility=Visibilité
visiblity_helper=Ce Référentiel est <span class="label label-red label-radius">Privé</span>
fork_repo=Référentiel de la fourche
fork_from=Fourche de
fork_visiblity_helper=Référentiel Aristide ne peut pas changer sa visiblité
fork_repo=Référentiel d'Embranchement
fork_from=Embranchement de
fork_visiblity_helper=Un Référentiel d'embranchement ne peut pas changer sa visiblité
repo_desc=Description
repo_lang=Langue
repo_lang_helper=Sélectionner un fichier .gitignore
@@ -377,29 +377,29 @@ diff.stats_desc=<strong> %d fichiers modifiés</strong> avec <strong>%d ajouts</
diff.bin=BIN
diff.view_file=Voir le fichier
release.releases=Releases
release.new_release=New Release
release.draft=Draft
release.prerelease=Pre-Release
release.releases=Versions
release.new_release=Nouvelle version
release.draft=Brouillon
release.prerelease=Pré-publication
release.stable=Stable
release.edit=edit
release.ahead=<strong>%d</strong> commits to %s since this release
release.source_code=Source Code
release.tag_name=Tag name
release.target=Target
release.tag_helper=Choose an existing tag, or create a new tag on publish.
release.release_title=Release title
release.content_with_md=Content with <a href="%s">Markdown</a>
release.write=Write
release.preview=Preview
release.content_placeholder=Write some content
release.loading=Loading...
release.prerelease_desc=This is a pre-release
release.prerelease_helper=Well point out that this release is identified as non-production ready.
release.publish=Publish Release
release.save_draft=Save Draft
release.edit_release=Edit Release
release.tag_name_already_exist=Release with this tag name has already existed.
release.edit=Éditer
release.ahead=<strong>%d</strong> commissions à %s depuis cette publication
release.source_code=Code Source
release.tag_name=Nom du tag
release.target=Cible 
release.tag_helper=Choisissez un tag existant ou créez-en un nouveau à publier.
release.release_title=Titre de la publication
release.content_with_md=Contenu avec <a href="%s">Démarque(s)</a>
release.write=Écrire
release.preview=Prévisualiser
release.content_placeholder=Rédiger du contenu
release.loading=Chargement…
release.prerelease_desc=Il s'agit d'une version préliminaire
release.prerelease_helper=Nous soulignerons que cette version est considérée comme non prête pour la production.
release.publish=Publier
release.save_draft=Sauvegarder le Brouillon
release.edit_release=Éditer la publication
release.tag_name_already_exist=Une publication avec ce nom de tag a déjà existée.
[org]
org_name_holder=Nom d'organisation

View File

@@ -377,29 +377,29 @@ diff.stats_desc=共有 <strong> %d 個文件被更改</strong>,包括 <strong>
diff.bin=二進制
diff.view_file=查看文件
release.releases=Releases
release.new_release=New Release
release.draft=Draft
release.prerelease=Pre-Release
release.stable=Stable
release.edit=edit
release.ahead=<strong>%d</strong> commits to %s since this release
release.source_code=Source Code
release.tag_name=Tag name
release.target=Target
release.tag_helper=Choose an existing tag, or create a new tag on publish.
release.release_title=Release title
release.content_with_md=Content with <a href="%s">Markdown</a>
release.write=Write
release.preview=Preview
release.content_placeholder=Write some content
release.loading=Loading...
release.prerelease_desc=This is a pre-release
release.prerelease_helper=Well point out that this release is identified as non-production ready.
release.publish=Publish Release
release.save_draft=Save Draft
release.edit_release=Edit Release
release.tag_name_already_exist=Release with this tag name has already existed.
release.releases=版本發佈
release.new_release=發佈新版本
release.draft=草稿
release.prerelease=預發佈版本
release.stable=穩定
release.edit=編輯
release.ahead=在該版本發佈之後已有 <strong>%d</strong> 次代碼提交到 %s 分支
release.source_code=源代碼
release.tag_name=標籤名稱
release.target=目標分支
release.tag_helper=選擇或創建一個已存在的標籤
release.release_title=發佈標題
release.content_with_md=使用 <a href="%s">Markdown</a> 編輯內容
release.write=內容編輯
release.preview=效果預覽
release.content_placeholder=請輸入內容
release.loading=正在加載...
release.prerelease_desc=這是一個預發佈版本
release.prerelease_helper=我們會告知用戶不建議將本發佈投入生產環境使用。
release.publish=發佈版本
release.save_draft=保在草稿
release.edit_release=編輯發佈信息
release.tag_name_already_exist=已經存在使用相同標籤的發佈版本。
[org]
org_name_holder=組織名稱

View File

@@ -30,7 +30,7 @@ The `config` file contains lines which will in the gogs docker container end up
Here you can define things like the MySQL server for your database block.
The `fig` file will just be added to `fig.yml`, which is used by fig to manage your containers.
This inculdes container linking!
This includes container linking!
Just have a look at them and it will be clear how to write your own blocks.
@@ -53,7 +53,7 @@ Example:
More sophisticated Example
--------------------------
Her is a more elaborated example
Here is a more elaborated example
```sh
./assemble_blocks.sh docker_gogs w_db_cache_session option_db_postgresql option_cache_redis option_session_mysql
@@ -86,4 +86,4 @@ This will pull in the `Dockerfile` from `docker_gogs` instead of the one from `d
`Dockerfile`s for the `master` and `dev` branch are provided as `docker_gogs` and `docker_gogs_dev`
[fig]:http://www.fig.sh/
[fig]:http://www.fig.sh/

View File

@@ -46,7 +46,7 @@ ENV HOME /home/git
ENV USER git
ENV PATH $GOGS_PATH:$PATH
RUN git config --global user.name "GoGS"
RUN git config --global user.name "GoGS" && git config --global user.email "gogitservice@gmail.com"
ENTRYPOINT ["/tmp/init_gogs.sh"]
CMD ["gogs", "web"]

View File

@@ -47,7 +47,7 @@ ENV HOME /home/git
ENV USER git
ENV PATH $GOGS_PATH:$PATH
RUN git config --global user.name "GoGS"
RUN git config --global user.name "GoGS" && git config --global user.email "gogitservice@gmail.com"
ENTRYPOINT ["/tmp/init_gogs.sh"]
CMD ["gogs", "web"]

View File

@@ -17,7 +17,7 @@ import (
"github.com/gogits/gogs/modules/setting"
)
const APP_VER = "0.5.9.1213 Beta"
const APP_VER = "0.5.11.0103 Beta"
func init() {
runtime.GOMAXPROCS(runtime.NumCPU())

View File

@@ -472,8 +472,8 @@ func UpdateIssueUserPairByAssignee(aid, iid int64) error {
if aid == 0 {
return nil
}
rawSql = "UPDATE `issue_user` SET is_assigned = true WHERE uid = ? AND issue_id = ?"
_, err := x.Exec(rawSql, aid, iid)
rawSql = "UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?"
_, err := x.Exec(rawSql, true, aid, iid)
return err
}

View File

@@ -45,22 +45,23 @@ func init() {
new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone),
new(Mirror), new(Release), new(LoginSource), new(Webhook),
new(UpdateTask), new(HookTask), new(Team), new(OrgUser), new(TeamUser),
new(Notice))
new(Notice), new(EmailAddress))
}
func LoadModelsConfig() {
DbCfg.Type = setting.Cfg.MustValue("database", "DB_TYPE")
sec := setting.Cfg.Section("database")
DbCfg.Type = sec.Key("DB_TYPE").String()
if DbCfg.Type == "sqlite3" {
UseSQLite3 = true
}
DbCfg.Host = setting.Cfg.MustValue("database", "HOST")
DbCfg.Name = setting.Cfg.MustValue("database", "NAME")
DbCfg.User = setting.Cfg.MustValue("database", "USER")
DbCfg.Host = sec.Key("HOST").String()
DbCfg.Name = sec.Key("NAME").String()
DbCfg.User = sec.Key("USER").String()
if len(DbCfg.Pwd) == 0 {
DbCfg.Pwd = setting.Cfg.MustValue("database", "PASSWD")
DbCfg.Pwd = sec.Key("PASSWD").String()
}
DbCfg.SslMode = setting.Cfg.MustValue("database", "SSL_MODE")
DbCfg.Path = setting.Cfg.MustValue("database", "PATH", "data/gogs.db")
DbCfg.SslMode = sec.Key("SSL_MODE").String()
DbCfg.Path = sec.Key("PATH").MustString("data/gogs.db")
}
func getEngine() (*xorm.Engine, error) {

View File

@@ -105,21 +105,18 @@ func NewRepoContext() {
log.Fatal(4, "Gogs requires Git version greater or equal to 1.7.1")
}
// Check if server has basic git setting and set if not.
if stdout, stderr, err := process.Exec("NewRepoContext(get setting)", "git", "config", "--get", "user.name"); err != nil || strings.TrimSpace(stdout) == "" {
// ExitError indicates user.name is not set
if _, ok := err.(*exec.ExitError); ok || strings.TrimSpace(stdout) == "" {
stndrdUserName := "Gogs"
stndrdUserEmail := "gogitservice@gmail.com"
if _, stderr, gerr := process.Exec("NewRepoContext(set name)", "git", "config", "--global", "user.name", stndrdUserName); gerr != nil {
log.Fatal(4, "Fail to set git user.name(%s): %s", gerr, stderr)
// Check if server has user.email and user.name set correctly and set if they're not.
for configKey, defaultValue := range map[string]string{"user.name": "Gogs", "user.email": "gogitservice@gmail.com"} {
if stdout, stderr, err := process.Exec("NewRepoContext(get setting)", "git", "config", "--get", configKey); err != nil || strings.TrimSpace(stdout) == "" {
// ExitError indicates this config is not set
if _, ok := err.(*exec.ExitError); ok || strings.TrimSpace(stdout) == "" {
if _, stderr, gerr := process.Exec("NewRepoContext(set "+configKey+")", "git", "config", "--global", configKey, defaultValue); gerr != nil {
log.Fatal(4, "Fail to set git %s(%s): %s", configKey, gerr, stderr)
}
log.Info("Git config %s set to %s", configKey, defaultValue)
} else {
log.Fatal(4, "Fail to get git %s(%s): %s", configKey, err, stderr)
}
if _, stderr, gerr := process.Exec("NewRepoContext(set email)", "git", "config", "--global", "user.email", stndrdUserEmail); gerr != nil {
log.Fatal(4, "Fail to set git user.email(%s): %s", gerr, stderr)
}
log.Info("Git user.name and user.email set to %s <%s>", stndrdUserName, stndrdUserEmail)
} else {
log.Fatal(4, "Fail to get git user.name(%s): %s", err, stderr)
}
}
@@ -832,6 +829,8 @@ func TransferOwnership(u *User, newOwner string, repo *Repository) error {
// ChangeRepositoryName changes all corresponding setting from old repository name to new one.
func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error) {
userName = strings.ToLower(userName)
oldRepoName = strings.ToLower(oldRepoName)
newRepoName = strings.ToLower(newRepoName)
if !IsLegalName(newRepoName) {
return ErrRepoNameIllegal
@@ -839,7 +838,7 @@ func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error)
// Update accesses.
accesses := make([]Access, 0, 10)
if err = x.Find(&accesses, &Access{RepoName: strings.ToLower(userName + "/" + oldRepoName)}); err != nil {
if err = x.Find(&accesses, &Access{RepoName: userName + "/" + oldRepoName}); err != nil {
return err
}
@@ -1187,6 +1186,8 @@ func MirrorUpdate() {
isMirrorUpdating = true
defer func() { isMirrorUpdating = false }()
mirrors := make([]*Mirror, 0, 10)
if err := x.Iterate(new(Mirror), func(idx int, bean interface{}) error {
m := bean.(*Mirror)
if m.NextUpdate.After(time.Now()) {
@@ -1197,13 +1198,25 @@ func MirrorUpdate() {
if _, stderr, err := process.ExecDir(10*time.Minute,
repoPath, fmt.Sprintf("MirrorUpdate: %s", repoPath),
"git", "remote", "update"); err != nil {
return errors.New("git remote update: " + stderr)
desc := fmt.Sprintf("Fail to update mirror repository(%s): %s", repoPath, stderr)
log.Error(4, desc)
if err = CreateRepositoryNotice(desc); err != nil {
log.Error(4, "Fail to add notice: %v", err)
}
return nil
}
m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour)
return UpdateMirror(m)
mirrors = append(mirrors, m)
return nil
}); err != nil {
log.Error(4, "repo.MirrorUpdate: %v", err)
log.Error(4, "MirrorUpdate: %v", err)
}
for i := range mirrors {
if err := UpdateMirror(mirrors[i]); err != nil {
log.Error(4, "UpdateMirror", fmt.Sprintf("%s: %v", mirrors[i].RepoName, err))
}
}
}
@@ -1215,7 +1228,7 @@ func GitFsck() {
isGitFscking = true
defer func() { isGitFscking = false }()
args := append([]string{"fsck"}, setting.GitFsckArgs...)
args := append([]string{"fsck"}, setting.Git.Fsck.Args...)
if err := x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error {
repo := bean.(*Repository)
@@ -1239,7 +1252,7 @@ func GitFsck() {
}
func GitGcRepos() error {
args := append([]string{"gc"}, setting.GitGcArgs...)
args := append([]string{"gc"}, setting.Git.GcArgs...)
return x.Where("id > 0").Iterate(new(Repository),
func(idx int, bean interface{}) error {
repo := bean.(*Repository)

View File

@@ -42,6 +42,8 @@ var (
ErrUserNotExist = errors.New("User does not exist")
ErrUserNotKeyOwner = errors.New("User does not the owner of public key")
ErrEmailAlreadyUsed = errors.New("E-mail already used")
ErrEmailNotExist = errors.New("E-mail does not exist")
ErrEmailNotActivated = errors.New("E-mail address has not been activated")
ErrUserNameIllegal = errors.New("User name contains illegal characters")
ErrLoginSourceNotExist = errors.New("Login source does not exist")
ErrLoginSourceNotActived = errors.New("Login source is not actived")
@@ -50,10 +52,11 @@ var (
// User represents the object of individual and member of organization.
type User struct {
Id int64
LowerName string `xorm:"UNIQUE NOT NULL"`
Name string `xorm:"UNIQUE NOT NULL"`
FullName string
Id int64
LowerName string `xorm:"UNIQUE NOT NULL"`
Name string `xorm:"UNIQUE NOT NULL"`
FullName string
// Email is the primary email address (to be used for communication).
Email string `xorm:"UNIQUE(s) NOT NULL"`
Passwd string `xorm:"NOT NULL"`
LoginType LoginType
@@ -93,6 +96,16 @@ type User struct {
Members []*User `xorm:"-"`
}
// EmailAdresses is the list of all email addresses of a user. Can contain the
// primary email address, but is not obligatory
type EmailAddress struct {
Id int64
Uid int64 `xorm:"INDEX NOT NULL"`
Email string `xorm:"UNIQUE NOT NULL"`
IsActivated bool
IsPrimary bool `xorm:"-"`
}
// DashboardLink returns the user dashboard page link.
func (u *User) DashboardLink() string {
if u.IsOrganization() {
@@ -248,6 +261,9 @@ func IsEmailUsed(email string) (bool, error) {
if len(email) == 0 {
return false, nil
}
if has, err := x.Get(&EmailAddress{Email: email}); has || err != nil {
return has, err
}
return x.Get(&User{Email: email})
}
@@ -355,6 +371,25 @@ func VerifyUserActiveCode(code string) (user *User) {
return nil
}
// verify active code when active account
func VerifyActiveEmailCode(code, email string) *EmailAddress {
minutes := setting.Service.ActiveCodeLives
if user := getVerifyUser(code); user != nil {
// time limit code
prefix := code[:base.TimeLimitCodeLength]
data := com.ToStr(user.Id) + email + user.LowerName + user.Passwd + user.Rands
if base.VerifyTimeLimitCode(data, minutes, prefix) {
emailAddress := &EmailAddress{Email: email}
if has, _ := x.Get(emailAddress); has {
return emailAddress
}
}
}
return nil
}
// ChangeUserName changes all corresponding setting from old user name to new one.
func ChangeUserName(u *User, newUserName string) (err error) {
if !IsLegalName(newUserName) {
@@ -418,7 +453,7 @@ func ChangeUserName(u *User, newUserName string) (err error) {
// UpdateUser updates user's information.
func UpdateUser(u *User) error {
has, err := x.Where("id != ?", u.Id).And("email = ?", u.Email).Get(new(User))
has, err := x.Where("id!=?", u.Id).And("type=?", INDIVIDUAL).And("email=?", u.Email).Get(new(User))
if err != nil {
return err
} else if has {
@@ -488,6 +523,10 @@ func DeleteUser(u *User) error {
if _, err = x.Delete(&Access{UserName: u.LowerName}); err != nil {
return err
}
// Delete all alternative email addresses
if _, err = x.Delete(&EmailAddress{Uid: u.Id}); err != nil {
return err
}
// Delete all SSH keys.
keys := make([]*PublicKey, 0, 10)
if err = x.Find(&keys, &PublicKey{OwnerId: u.Id}); err != nil {
@@ -508,9 +547,12 @@ func DeleteUser(u *User) error {
return err
}
// DeleteInactivateUsers deletes all inactivate users.
// DeleteInactivateUsers deletes all inactivate users and email addresses.
func DeleteInactivateUsers() error {
_, err := x.Where("is_active=?", false).Delete(new(User))
if err == nil {
_, err = x.Where("is_activated=?", false).Delete(new(EmailAddress))
}
return err
}
@@ -584,6 +626,117 @@ func GetUserIdsByNames(names []string) []int64 {
return ids
}
// Get all email addresses
func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
emails := make([]*EmailAddress, 0, 5)
err := x.Where("uid=?", uid).Find(&emails)
if err != nil {
return nil, err
}
u, err := GetUserById(uid)
if err != nil {
return nil, err
}
isPrimaryFound := false
for _, email := range emails {
if email.Email == u.Email {
isPrimaryFound = true
email.IsPrimary = true
} else {
email.IsPrimary = false
}
}
// We alway want the primary email address displayed, even if it's not in
// the emailaddress table (yet)
if !isPrimaryFound {
emails = append(emails, &EmailAddress{Email: u.Email, IsActivated: true, IsPrimary: true})
}
return emails, nil
}
func AddEmailAddress(email *EmailAddress) error {
used, err := IsEmailUsed(email.Email)
if err != nil {
return err
} else if used {
return ErrEmailAlreadyUsed
}
_, err = x.Insert(email)
return err
}
func (email *EmailAddress) Activate() error {
email.IsActivated = true
if _, err := x.Id(email.Id).AllCols().Update(email); err != nil {
return err
}
if user, err := GetUserById(email.Uid); err != nil {
return err
} else {
user.Rands = GetUserSalt()
return UpdateUser(user)
}
}
func DeleteEmailAddress(email *EmailAddress) error {
has, err := x.Get(email)
if err != nil {
return err
} else if !has {
return ErrEmailNotExist
}
if _, err = x.Delete(email); err != nil {
return err
}
return nil
}
func MakeEmailPrimary(email *EmailAddress) error {
has, err := x.Get(email)
if err != nil {
return err
} else if !has {
return ErrEmailNotExist
}
if !email.IsActivated {
return ErrEmailNotActivated
}
user := &User{Id: email.Uid}
has, err = x.Get(user)
if err != nil {
return err
} else if !has {
return ErrUserNotExist
}
// Make sure the former primary email doesn't disappear
former_primary_email := &EmailAddress{Email: user.Email}
has, err = x.Get(former_primary_email)
if err != nil {
return err
} else if !has {
former_primary_email.Uid = user.Id
former_primary_email.IsActivated = user.IsActive
x.Insert(former_primary_email)
}
user.Email = email.Email
_, err = x.Id(user.Id).AllCols().Update(user)
return err
}
// UserCommit represents a commit with validation of user.
type UserCommit struct {
User *User
@@ -629,14 +782,27 @@ func GetUserByEmail(email string) (*User, error) {
if len(email) == 0 {
return nil, ErrUserNotExist
}
// First try to find the user by primary email
user := &User{Email: strings.ToLower(email)}
has, err := x.Get(user)
if err != nil {
return nil, err
} else if !has {
return nil, ErrUserNotExist
}
return user, nil
if has {
return user, nil
}
// Otherwise, check in alternative list for activated email addresses
emailAddress := &EmailAddress{Email: strings.ToLower(email), IsActivated: true}
has, err = x.Get(emailAddress)
if err != nil {
return nil, err
}
if has {
return GetUserById(emailAddress.Uid)
}
return nil, ErrUserNotExist
}
// SearchUserByName returns given number of users whose name contains keyword.

View File

@@ -49,19 +49,19 @@ func validateApiReq(errs binding.Errors, data map[string]interface{}, f auth.For
if errs[0].FieldNames[0] == field.Name {
switch errs[0].Classification {
case binding.RequiredError:
case binding.ERR_REQUIRED:
data["ErrorMsg"] = fieldName + " cannot be empty"
case binding.AlphaDashError:
case binding.ERR_ALPHA_DASH:
data["ErrorMsg"] = fieldName + " must be valid alpha or numeric or dash(-_) characters"
case binding.AlphaDashDotError:
case binding.ERR_ALPHA_DASH_DOT:
data["ErrorMsg"] = fieldName + " must be valid alpha or numeric or dash(-_) or dot characters"
case binding.MinSizeError:
case binding.ERR_MIN_SIZE:
data["ErrorMsg"] = fieldName + " must contain at least " + auth.GetMinSize(field) + " characters"
case binding.MaxSizeError:
case binding.ERR_MAX_SIZE:
data["ErrorMsg"] = fieldName + " must contain at most " + auth.GetMaxSize(field) + " characters"
case binding.EmailError:
case binding.ERR_EMAIL:
data["ErrorMsg"] = fieldName + " is not a valid e-mail address"
case binding.UrlError:
case binding.ERR_URL:
data["ErrorMsg"] = fieldName + " is not a valid URL"
default:
data["ErrorMsg"] = "Unknown error: " + errs[0].Classification

View File

@@ -204,19 +204,19 @@ func validate(errs binding.Errors, data map[string]interface{}, f Form, l macaro
data["Err_"+field.Name] = true
trName := l.Tr("form." + field.Name)
switch errs[0].Classification {
case binding.RequiredError:
case binding.ERR_REQUIRED:
data["ErrorMsg"] = trName + l.Tr("form.require_error")
case binding.AlphaDashError:
case binding.ERR_ALPHA_DASH:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
case binding.AlphaDashDotError:
case binding.ERR_ALPHA_DASH_DOT:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
case binding.MinSizeError:
case binding.ERR_MIN_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.min_size_error", GetMinSize(field))
case binding.MaxSizeError:
case binding.ERR_MAX_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.max_size_error", GetMaxSize(field))
case binding.EmailError:
case binding.ERR_EMAIL:
data["ErrorMsg"] = trName + l.Tr("form.email_error")
case binding.UrlError:
case binding.ERR_URL:
data["ErrorMsg"] = trName + l.Tr("form.url_error")
default:
data["ErrorMsg"] = l.Tr("form.unknown_error") + " " + errs[0].Classification

View File

@@ -97,6 +97,14 @@ func (f *UploadAvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) b
return validate(errs, ctx.Data, f, ctx.Locale)
}
type AddEmailForm struct {
Email string `form:"email" binding:"Required;Email;MaxSize(50)"`
}
func (f *AddEmailForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
return validate(errs, ctx.Data, f, ctx.Locale)
}
type ChangePasswordForm struct {
OldPassword string `form:"old_password" binding:"Required;MinSize(6);MaxSize(255)"`
Password string `form:"password" binding:"Required;MinSize(6);MaxSize(255)"`

View File

@@ -16,7 +16,9 @@ var c = New()
func NewCronContext() {
c.AddFunc("Update mirrors", "@every 1h", models.MirrorUpdate)
c.AddFunc("Deliver hooks", fmt.Sprintf("@every %dm", setting.WebhookTaskInterval), models.DeliverHooks)
c.AddFunc("Repository health check", "@every 1h", models.GitFsck)
if setting.Git.Fsck.Enable {
c.AddFunc("Repository health check", fmt.Sprintf("@every %dh", setting.Git.Fsck.Interval), models.GitFsck)
}
c.Start()
}

View File

@@ -21,6 +21,7 @@ import (
const (
AUTH_ACTIVE base.TplName = "mail/auth/active"
AUTH_ACTIVATE_EMAIL base.TplName = "mail/auth/activate_email"
AUTH_REGISTER_SUCCESS base.TplName = "mail/auth/register_success"
AUTH_RESET_PASSWORD base.TplName = "mail/auth/reset_passwd"
@@ -30,9 +31,7 @@ const (
// Create New mail message use MailFrom and MailUser
func NewMailMessageFrom(To []string, from, subject, body string) Message {
msg := NewHtmlMessage(To, from, subject, body)
msg.User = setting.MailService.User
return msg
return NewHtmlMessage(To, from, subject, body)
}
// Create New mail message use MailFrom and MailUser
@@ -64,6 +63,17 @@ func CreateUserActiveCode(u *models.User, startInf interface{}) string {
return code
}
// create a time limit code for user active
func CreateUserEmailActivateCode(u *models.User, e *models.EmailAddress, startInf interface{}) string {
minutes := setting.Service.ActiveCodeLives
data := com.ToStr(u.Id) + e.Email + u.LowerName + u.Passwd + u.Rands
code := base.CreateTimeLimitCode(data, minutes, startInf)
// add tail hex username
code += hex.EncodeToString([]byte(u.LowerName))
return code
}
// Send user register mail with active code
func SendRegisterMail(r macaron.Render, u *models.User) {
code := CreateUserActiveCode(u, nil)
@@ -103,6 +113,27 @@ func SendActiveMail(r macaron.Render, u *models.User) {
SendAsync(&msg)
}
// Send email to verify secondary email.
func SendActivateEmail(r macaron.Render, user *models.User, email *models.EmailAddress) {
code := CreateUserEmailActivateCode(user, email, nil)
subject := "Verify your e-mail address"
data := GetMailTmplData(user)
data["Code"] = code
data["Email"] = email.Email
body, err := r.HTMLString(string(AUTH_ACTIVATE_EMAIL), data)
if err != nil {
log.Error(4, "mail.SendActiveMail(fail to render): %v", err)
return
}
msg := NewMailMessage([]string{email.Email}, subject, body)
msg.Info = fmt.Sprintf("UID: %d, send activate email to %s", user.Id, email.Email)
SendAsync(&msg)
}
// Send reset password email.
func SendResetPasswdMail(r macaron.Render, u *models.User) {
code := CreateUserActiveCode(u, nil)

View File

@@ -8,6 +8,7 @@ import (
"crypto/tls"
"fmt"
"net"
"net/mail"
"net/smtp"
"strings"
@@ -20,7 +21,6 @@ type Message struct {
From string
Subject string
Body string
User string
Type string
Massive bool
Info string
@@ -35,15 +35,14 @@ func (m Message) Content() string {
}
// create mail content
content := "From: \"" + m.From + "\" <" + m.User +
">\r\nSubject: " + m.Subject + "\r\nContent-Type: " + contentType + "\r\n\r\n" + m.Body
content := "From: " + m.From + "\r\nSubject: " + m.Subject + "\r\nContent-Type: " + contentType + "\r\n\r\n" + m.Body
return content
}
var mailQueue chan *Message
func NewMailerContext() {
mailQueue = make(chan *Message, setting.Cfg.MustInt("mailer", "SEND_BUFFER_LEN", 10))
mailQueue = make(chan *Message, setting.Cfg.Section("mailer").Key("SEND_BUFFER_LEN").MustInt(10))
go processMailQueue()
}
@@ -67,29 +66,67 @@ func processMailQueue() {
}
// sendMail allows mail with self-signed certificates.
func sendMail(hostAddressWithPort string, auth smtp.Auth, from string, recipients []string, msgContent []byte) error {
client, err := smtp.Dial(hostAddressWithPort)
func sendMail(settings *setting.Mailer, recipients []string, msgContent []byte) error {
host, port, err := net.SplitHostPort(settings.Host)
if err != nil {
return err
}
host, _, _ := net.SplitHostPort(hostAddressWithPort)
tlsConn := &tls.Config{
InsecureSkipVerify: true,
tlsconfig := &tls.Config{
InsecureSkipVerify: settings.SkipVerify,
ServerName: host,
}
if err = client.StartTLS(tlsConn); err != nil {
conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
if err != nil {
return err
}
defer conn.Close()
isSecureConn := false
// Start TLS directly if the port ends with 465 (SMTPS protocol)
if strings.HasSuffix(port, "465") {
conn = tls.Client(conn, tlsconfig)
isSecureConn = true
}
client, err := smtp.NewClient(conn, host)
if err != nil {
return err
}
if ok, _ := client.Extension("AUTH"); ok && auth != nil {
if err = client.Auth(auth); err != nil {
// If not using SMTPS, alway use STARTTLS if available
hasStartTLS, _ := client.Extension("STARTTLS")
if !isSecureConn && hasStartTLS {
if err = client.StartTLS(tlsconfig); err != nil {
return err
}
}
if err = client.Mail(from); err != nil {
canAuth, options := client.Extension("AUTH")
if canAuth && len(settings.User) > 0 {
var auth smtp.Auth
if strings.Contains(options, "CRAM-MD5") {
auth = smtp.CRAMMD5Auth(settings.User, settings.Passwd)
} else if strings.Contains(options, "PLAIN") {
auth = smtp.PlainAuth("", settings.User, settings.Passwd, host)
}
if auth != nil {
if err = client.Auth(auth); err != nil {
return err
}
}
}
if fromAddress, err := mail.ParseAddress(settings.From); err != nil {
return err
} else {
if err = client.Mail(fromAddress.Address); err != nil {
return err
}
}
for _, rec := range recipients {
@@ -116,7 +153,6 @@ func sendMail(hostAddressWithPort string, auth smtp.Auth, from string, recipient
// Direct Send mail message
func Send(msg *Message) (int, error) {
log.Trace("Sending mails to: %s", strings.Join(msg.To, "; "))
host := strings.Split(setting.MailService.Host, ":")
// get message body
content := msg.Content()
@@ -127,17 +163,12 @@ func Send(msg *Message) (int, error) {
return 0, fmt.Errorf("empty email body")
}
var auth smtp.Auth
if len(setting.MailService.Passwd) > 0 {
auth = smtp.PlainAuth("", setting.MailService.User, setting.MailService.Passwd, host[0])
}
if msg.Massive {
// send mail to multiple emails one by one
num := 0
for _, to := range msg.To {
body := []byte("To: " + to + "\r\n" + content)
err := sendMail(setting.MailService.Host, auth, msg.From, []string{to}, body)
err := sendMail(setting.MailService, []string{to}, body)
if err != nil {
return num, err
}
@@ -148,7 +179,7 @@ func Send(msg *Message) (int, error) {
body := []byte("To: " + strings.Join(msg.To, ";") + "\r\n" + content)
// send to multiple emails in one message
err := sendMail(setting.MailService.Host, auth, msg.From, msg.To, body)
err := sendMail(setting.MailService, msg.To, body)
if err != nil {
return 0, err
} else {

View File

@@ -16,9 +16,9 @@ import (
"time"
"github.com/Unknwon/com"
"github.com/Unknwon/goconfig"
"github.com/macaron-contrib/oauth2"
"github.com/macaron-contrib/session"
"gopkg.in/ini.v1"
"github.com/gogits/gogs/modules/log"
// "github.com/gogits/gogs/modules/ssh"
@@ -104,19 +104,24 @@ var (
EnableMemcache bool
// Session settings.
SessionProvider string
SessionConfig *session.Config
SessionConfig session.Options
// Git settings.
MaxGitDiffLines int
GitFsckArgs []string
GitGcArgs []string
Git struct {
MaxGitDiffLines int
GcArgs []string `delim:" "`
Fsck struct {
Enable bool
Interval int
Args []string `delim:" "`
} `ini:"git.fsck"`
}
// I18n settings.
Langs, Names []string
// Global setting objects.
Cfg *goconfig.ConfigFile
Cfg *ini.File
ConfRootPath string
CustomPath string // Custom directory path.
ProdMode bool
@@ -157,7 +162,7 @@ func NewConfigContext() {
}
ConfRootPath = path.Join(workDir, "conf")
Cfg, err = goconfig.LoadConfigFile(path.Join(workDir, "conf/app.ini"))
Cfg, err = ini.Load(path.Join(workDir, "conf/app.ini"))
if err != nil {
log.Fatal(4, "Fail to parse 'conf/app.ini': %v", err)
}
@@ -169,15 +174,19 @@ func NewConfigContext() {
cfgPath := path.Join(CustomPath, "conf/app.ini")
if com.IsFile(cfgPath) {
if err = Cfg.AppendFiles(cfgPath); err != nil {
if err = Cfg.Append(cfgPath); err != nil {
log.Fatal(4, "Fail to load custom 'conf/app.ini': %v", err)
}
} else {
log.Warn("No custom 'conf/app.ini' found, please go to '/install'")
}
Cfg.NameMapper = ini.AllCapsUnderscore
AppName = Cfg.MustValue("", "APP_NAME", "Gogs: Go Git Service")
AppUrl = Cfg.MustValue("server", "ROOT_URL", "http://localhost:3000/")
LogRootPath = Cfg.Section("log").Key("ROOT_PATH").MustString(path.Join(workDir, "log"))
sec := Cfg.Section("server")
AppName = Cfg.Section("").Key("APP_NAME").MustString("Gogs: Go Git Service")
AppUrl = sec.Key("ROOT_URL").MustString("http://localhost:3000/")
if AppUrl[len(AppUrl)-1] != '/' {
AppUrl += "/"
}
@@ -190,43 +199,43 @@ func NewConfigContext() {
AppSubUrl = strings.TrimSuffix(url.Path, "/")
Protocol = HTTP
if Cfg.MustValue("server", "PROTOCOL") == "https" {
if sec.Key("PROTOCOL").String() == "https" {
Protocol = HTTPS
CertFile = Cfg.MustValue("server", "CERT_FILE")
KeyFile = Cfg.MustValue("server", "KEY_FILE")
}
if Cfg.MustValue("server", "PROTOCOL") == "fcgi" {
CertFile = sec.Key("CERT_FILE").String()
KeyFile = sec.Key("KEY_FILE").String()
} else if sec.Key("PROTOCOL").String() == "fcgi" {
Protocol = FCGI
}
Domain = Cfg.MustValue("server", "DOMAIN", "localhost")
HttpAddr = Cfg.MustValue("server", "HTTP_ADDR", "0.0.0.0")
HttpPort = Cfg.MustValue("server", "HTTP_PORT", "3000")
SshPort = Cfg.MustInt("server", "SSH_PORT", 22)
OfflineMode = Cfg.MustBool("server", "OFFLINE_MODE")
DisableRouterLog = Cfg.MustBool("server", "DISABLE_ROUTER_LOG")
StaticRootPath = Cfg.MustValue("server", "STATIC_ROOT_PATH", workDir)
LogRootPath = Cfg.MustValue("log", "ROOT_PATH", path.Join(workDir, "log"))
EnableGzip = Cfg.MustBool("server", "ENABLE_GZIP")
Domain = sec.Key("DOMAIN").MustString("localhost")
HttpAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
HttpPort = sec.Key("HTTP_PORT").MustString("3000")
SshPort = sec.Key("SSH_PORT").MustInt(22)
OfflineMode = sec.Key("OFFLINE_MODE").MustBool()
DisableRouterLog = sec.Key("DISABLE_ROUTER_LOG").MustBool()
StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(workDir)
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
switch Cfg.MustValue("server", "LANDING_PAGE", "home") {
switch sec.Key("LANDING_PAGE").MustString("home") {
case "explore":
LandingPageUrl = LANDING_PAGE_EXPLORE
default:
LandingPageUrl = LANDING_PAGE_HOME
}
InstallLock = Cfg.MustBool("security", "INSTALL_LOCK")
SecretKey = Cfg.MustValue("security", "SECRET_KEY")
LogInRememberDays = Cfg.MustInt("security", "LOGIN_REMEMBER_DAYS")
CookieUserName = Cfg.MustValue("security", "COOKIE_USERNAME")
CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME")
ReverseProxyAuthUser = Cfg.MustValue("security", "REVERSE_PROXY_AUTHENTICATION_USER", "X-WEBAUTH-USER")
sec = Cfg.Section("security")
InstallLock = sec.Key("INSTALL_LOCK").MustBool()
SecretKey = sec.Key("SECRET_KEY").String()
LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt()
CookieUserName = sec.Key("COOKIE_USERNAME").String()
CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").String()
ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER")
AttachmentPath = Cfg.MustValue("attachment", "PATH", "data/attachments")
AttachmentAllowedTypes = Cfg.MustValue("attachment", "ALLOWED_TYPES", "image/jpeg|image/png")
AttachmentMaxSize = Cfg.MustInt64("attachment", "MAX_SIZE", 32)
AttachmentMaxFiles = Cfg.MustInt("attachment", "MAX_FILES", 10)
AttachmentEnabled = Cfg.MustBool("attachment", "ENABLE", true)
sec = Cfg.Section("attachment")
AttachmentPath = sec.Key("PATH").MustString("data/attachments")
AttachmentAllowedTypes = sec.Key("ALLOWED_TYPES").MustString("image/jpeg|image/png")
AttachmentMaxSize = sec.Key("MAX_SIZE").MustInt64(32)
AttachmentMaxFiles = sec.Key("MAX_FILES").MustInt(10)
AttachmentEnabled = sec.Key("ENABLE").MustBool(true)
TimeFormat = map[string]string{
"ANSIC": time.ANSIC,
@@ -244,13 +253,13 @@ func NewConfigContext() {
"StampMilli": time.StampMilli,
"StampMicro": time.StampMicro,
"StampNano": time.StampNano,
}[Cfg.MustValue("time", "FORMAT", "RFC1123")]
}[Cfg.Section("time").Key("FORMAT").MustString("RFC1123")]
if err = os.MkdirAll(AttachmentPath, os.ModePerm); err != nil {
log.Fatal(4, "Could not create directory %s: %s", AttachmentPath, err)
}
RunUser = Cfg.MustValue("", "RUN_USER")
RunUser = Cfg.Section("").Key("RUN_USER").String()
curUser := os.Getenv("USER")
if len(curUser) == 0 {
curUser = os.Getenv("USERNAME")
@@ -265,36 +274,36 @@ func NewConfigContext() {
if err != nil {
log.Fatal(4, "Fail to get home directory: %v", err)
}
RepoRootPath = Cfg.MustValue("repository", "ROOT", filepath.Join(homeDir, "gogs-repositories"))
sec = Cfg.Section("repository")
RepoRootPath = sec.Key("ROOT").MustString(filepath.Join(homeDir, "gogs-repositories"))
if !filepath.IsAbs(RepoRootPath) {
RepoRootPath = filepath.Join(workDir, RepoRootPath)
} else {
RepoRootPath = filepath.Clean(RepoRootPath)
}
if err = os.MkdirAll(RepoRootPath, os.ModePerm); err != nil {
log.Fatal(4, "Fail to create repository root path(%s): %v", RepoRootPath, err)
}
ScriptType = Cfg.MustValue("repository", "SCRIPT_TYPE", "bash")
ScriptType = sec.Key("SCRIPT_TYPE").MustString("bash")
PictureService = Cfg.MustValueRange("picture", "SERVICE", "server", []string{"server"})
AvatarUploadPath = Cfg.MustValue("picture", "AVATAR_UPLOAD_PATH", "data/avatars")
sec = Cfg.Section("picture")
PictureService = sec.Key("SERVICE").In("server", []string{"server"})
AvatarUploadPath = sec.Key("AVATAR_UPLOAD_PATH").MustString("data/avatars")
os.MkdirAll(AvatarUploadPath, os.ModePerm)
switch Cfg.MustValue("picture", "GRAVATAR_SOURCE", "gravatar") {
switch sec.Key("GRAVATAR_SOURCE").MustString("gravatar") {
case "duoshuo":
GravatarSource = "http://gravatar.duoshuo.com/avatar/"
default:
GravatarSource = "//1.gravatar.com/avatar/"
}
DisableGravatar = Cfg.MustBool("picture", "DISABLE_GRAVATAR")
DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool()
MaxGitDiffLines = Cfg.MustInt("git", "MAX_GITDIFF_LINES", 10000)
GitFsckArgs = Cfg.MustValueArray("git", "FSCK_ARGS", " ")
GitGcArgs = Cfg.MustValueArray("git", "GC_ARGS", " ")
if err = Cfg.Section("git").MapTo(&Git); err != nil {
log.Fatal(4, "Fail to map Git settings: %v", err)
}
Langs = Cfg.MustValueArray("i18n", "LANGS", ",")
Names = Cfg.MustValueArray("i18n", "NAMES", ",")
Langs = Cfg.Section("i18n").Key("LANGS").Strings(",")
Names = Cfg.Section("i18n").Key("NAMES").Strings(",")
HasRobotsTxt = com.IsFile(path.Join(CustomPath, "robots.txt"))
}
@@ -312,13 +321,13 @@ var Service struct {
}
func newService() {
Service.ActiveCodeLives = Cfg.MustInt("service", "ACTIVE_CODE_LIVE_MINUTES", 180)
Service.ResetPwdCodeLives = Cfg.MustInt("service", "RESET_PASSWD_CODE_LIVE_MINUTES", 180)
Service.DisableRegistration = Cfg.MustBool("service", "DISABLE_REGISTRATION")
Service.RequireSignInView = Cfg.MustBool("service", "REQUIRE_SIGNIN_VIEW")
Service.EnableCacheAvatar = Cfg.MustBool("service", "ENABLE_CACHE_AVATAR")
Service.EnableReverseProxyAuth = Cfg.MustBool("service", "ENABLE_REVERSE_PROXY_AUTHENTICATION")
Service.EnableReverseProxyAutoRegister = Cfg.MustBool("service", "ENABLE_REVERSE_PROXY_AUTO_REGISTERATION")
Service.ActiveCodeLives = Cfg.Section("service").Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180)
Service.ResetPwdCodeLives = Cfg.Section("service").Key("RESET_PASSWD_CODE_LIVE_MINUTES").MustInt(180)
Service.DisableRegistration = Cfg.Section("service").Key("DISABLE_REGISTRATION").MustBool()
Service.RequireSignInView = Cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").MustBool()
Service.EnableCacheAvatar = Cfg.Section("service").Key("ENABLE_CACHE_AVATAR").MustBool()
Service.EnableReverseProxyAuth = Cfg.Section("service").Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
Service.EnableReverseProxyAutoRegister = Cfg.Section("service").Key("ENABLE_REVERSE_PROXY_AUTO_REGISTERATION").MustBool()
}
var logLevels = map[string]string{
@@ -334,17 +343,17 @@ func newLogService() {
log.Info("%s %s", AppName, AppVer)
// Get and check log mode.
LogModes = strings.Split(Cfg.MustValue("log", "MODE", "console"), ",")
LogModes = strings.Split(Cfg.Section("log").Key("MODE").MustString("console"), ",")
LogConfigs = make([]string, len(LogModes))
for i, mode := range LogModes {
mode = strings.TrimSpace(mode)
modeSec := "log." + mode
if _, err := Cfg.GetSection(modeSec); err != nil {
sec, err := Cfg.GetSection("log." + mode)
if err != nil {
log.Fatal(4, "Unknown log mode: %s", mode)
}
// Log level.
levelName := Cfg.MustValueRange("log."+mode, "LEVEL", "Trace",
levelName := Cfg.Section("log."+mode).Key("LEVEL").In("Trace",
[]string{"Trace", "Debug", "Info", "Warn", "Error", "Critical"})
level, ok := logLevels[levelName]
if !ok {
@@ -356,42 +365,42 @@ func newLogService() {
case "console":
LogConfigs[i] = fmt.Sprintf(`{"level":%s}`, level)
case "file":
logPath := Cfg.MustValue(modeSec, "FILE_NAME", path.Join(LogRootPath, "gogs.log"))
logPath := sec.Key("FILE_NAME").MustString(path.Join(LogRootPath, "gogs.log"))
os.MkdirAll(path.Dir(logPath), os.ModePerm)
LogConfigs[i] = fmt.Sprintf(
`{"level":%s,"filename":"%s","rotate":%v,"maxlines":%d,"maxsize":%d,"daily":%v,"maxdays":%d}`, level,
logPath,
Cfg.MustBool(modeSec, "LOG_ROTATE", true),
Cfg.MustInt(modeSec, "MAX_LINES", 1000000),
1<<uint(Cfg.MustInt(modeSec, "MAX_SIZE_SHIFT", 28)),
Cfg.MustBool(modeSec, "DAILY_ROTATE", true),
Cfg.MustInt(modeSec, "MAX_DAYS", 7))
sec.Key("LOG_ROTATE").MustBool(true),
sec.Key("MAX_LINES").MustInt(1000000),
1<<uint(sec.Key("MAX_SIZE_SHIFT").MustInt(28)),
sec.Key("DAILY_ROTATE").MustBool(true),
sec.Key("MAX_DAYS").MustInt(7))
case "conn":
LogConfigs[i] = fmt.Sprintf(`{"level":%s,"reconnectOnMsg":%v,"reconnect":%v,"net":"%s","addr":"%s"}`, level,
Cfg.MustBool(modeSec, "RECONNECT_ON_MSG"),
Cfg.MustBool(modeSec, "RECONNECT"),
Cfg.MustValueRange(modeSec, "PROTOCOL", "tcp", []string{"tcp", "unix", "udp"}),
Cfg.MustValue(modeSec, "ADDR", ":7020"))
sec.Key("RECONNECT_ON_MSG").MustBool(),
sec.Key("RECONNECT").MustBool(),
sec.Key("PROTOCOL").In("tcp", []string{"tcp", "unix", "udp"}),
sec.Key("ADDR").MustString(":7020"))
case "smtp":
LogConfigs[i] = fmt.Sprintf(`{"level":%s,"username":"%s","password":"%s","host":"%s","sendTos":"%s","subject":"%s"}`, level,
Cfg.MustValue(modeSec, "USER", "example@example.com"),
Cfg.MustValue(modeSec, "PASSWD", "******"),
Cfg.MustValue(modeSec, "HOST", "127.0.0.1:25"),
Cfg.MustValue(modeSec, "RECEIVERS", "[]"),
Cfg.MustValue(modeSec, "SUBJECT", "Diagnostic message from serve"))
sec.Key("USER").MustString("example@example.com"),
sec.Key("PASSWD").MustString("******"),
sec.Key("HOST").MustString("127.0.0.1:25"),
sec.Key("RECEIVERS").MustString("[]"),
sec.Key("SUBJECT").MustString("Diagnostic message from serve"))
case "database":
LogConfigs[i] = fmt.Sprintf(`{"level":%s,"driver":"%s","conn":"%s"}`, level,
Cfg.MustValue(modeSec, "DRIVER"),
Cfg.MustValue(modeSec, "CONN"))
sec.Key("DRIVER").String(),
sec.Key("CONN").String())
}
log.NewLogger(Cfg.MustInt64("log", "BUFFER_LEN", 10000), mode, LogConfigs[i])
log.NewLogger(Cfg.Section("log").Key("BUFFER_LEN").MustInt64(10000), mode, LogConfigs[i])
log.Info("Log Mode: %s(%s)", strings.Title(mode), levelName)
}
}
func newCacheService() {
CacheAdapter = Cfg.MustValueRange("cache", "ADAPTER", "memory", []string{"memory", "redis", "memcache"})
CacheAdapter = Cfg.Section("cache").Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache"})
if EnableRedis {
log.Info("Redis Enabled")
}
@@ -401,9 +410,9 @@ func newCacheService() {
switch CacheAdapter {
case "memory":
CacheInternal = Cfg.MustInt("cache", "INTERVAL", 60)
CacheInternal = Cfg.Section("cache").Key("INTERVAL").MustInt(60)
case "redis", "memcache":
CacheConn = strings.Trim(Cfg.MustValue("cache", "HOST"), "\" ")
CacheConn = strings.Trim(Cfg.Section("cache").Key("HOST").String(), "\" ")
default:
log.Fatal(4, "Unknown cache adapter: %s", CacheAdapter)
}
@@ -412,21 +421,14 @@ func newCacheService() {
}
func newSessionService() {
SessionProvider = Cfg.MustValueRange("session", "PROVIDER", "memory",
SessionConfig.Provider = Cfg.Section("session").Key("PROVIDER").In("memory",
[]string{"memory", "file", "redis", "mysql"})
SessionConfig = new(session.Config)
SessionConfig.ProviderConfig = strings.Trim(Cfg.MustValue("session", "PROVIDER_CONFIG"), "\" ")
SessionConfig.CookieName = Cfg.MustValue("session", "COOKIE_NAME", "i_like_gogits")
SessionConfig.ProviderConfig = strings.Trim(Cfg.Section("session").Key("PROVIDER_CONFIG").String(), "\" ")
SessionConfig.CookieName = Cfg.Section("session").Key("COOKIE_NAME").MustString("i_like_gogits")
SessionConfig.CookiePath = AppSubUrl
SessionConfig.Secure = Cfg.MustBool("session", "COOKIE_SECURE")
SessionConfig.EnableSetCookie = Cfg.MustBool("session", "ENABLE_SET_COOKIE", true)
SessionConfig.Gclifetime = Cfg.MustInt64("session", "GC_INTERVAL_TIME", 86400)
SessionConfig.Maxlifetime = Cfg.MustInt64("session", "SESSION_LIFE_TIME", 86400)
if SessionProvider == "file" {
os.MkdirAll(path.Dir(SessionConfig.ProviderConfig), os.ModePerm)
}
SessionConfig.Secure = Cfg.Section("session").Key("COOKIE_SECURE").MustBool()
SessionConfig.Gclifetime = Cfg.Section("session").Key("GC_INTERVAL_TIME").MustInt64(86400)
SessionConfig.Maxlifetime = Cfg.Section("session").Key("SESSION_LIFE_TIME").MustInt64(86400)
log.Info("Session Service Enabled")
}
@@ -437,6 +439,7 @@ type Mailer struct {
Host string
From string
User, Passwd string
SkipVerify bool
}
type OauthInfo struct {
@@ -457,23 +460,25 @@ var (
)
func newMailService() {
sec := Cfg.Section("mailer")
// Check mailer setting.
if !Cfg.MustBool("mailer", "ENABLED") {
if !sec.Key("ENABLED").MustBool() {
return
}
MailService = &Mailer{
Name: Cfg.MustValue("mailer", "NAME", AppName),
Host: Cfg.MustValue("mailer", "HOST"),
User: Cfg.MustValue("mailer", "USER"),
Passwd: Cfg.MustValue("mailer", "PASSWD"),
Name: sec.Key("NAME").MustString(AppName),
Host: sec.Key("HOST").String(),
User: sec.Key("USER").String(),
Passwd: sec.Key("PASSWD").String(),
SkipVerify: sec.Key("SKIP_VERIFY").MustBool(),
}
MailService.From = Cfg.MustValue("mailer", "FROM", MailService.User)
MailService.From = sec.Key("FROM").MustString(MailService.User)
log.Info("Mail Service Enabled")
}
func newRegisterMailService() {
if !Cfg.MustBool("service", "REGISTER_EMAIL_CONFIRM") {
if !Cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").MustBool() {
return
} else if MailService == nil {
log.Warn("Register Mail Service: Mail Service is not enabled")
@@ -484,7 +489,7 @@ func newRegisterMailService() {
}
func newNotifyMailService() {
if !Cfg.MustBool("service", "ENABLE_NOTIFY_MAIL") {
if !Cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").MustBool() {
return
} else if MailService == nil {
log.Warn("Notify Mail Service: Mail Service is not enabled")
@@ -495,8 +500,8 @@ func newNotifyMailService() {
}
func newWebhookService() {
WebhookTaskInterval = Cfg.MustInt("webhook", "TASK_INTERVAL", 1)
WebhookDeliverTimeout = Cfg.MustInt("webhook", "DELIVER_TIMEOUT", 5)
WebhookTaskInterval = Cfg.Section("webhook").Key("TASK_INTERVAL").MustInt(1)
WebhookDeliverTimeout = Cfg.Section("webhook").Key("DELIVER_TIMEOUT").MustInt(5)
}
func NewServices() {

View File

@@ -35,7 +35,7 @@ var (
)
func NewOauthService() {
if !setting.Cfg.MustBool("oauth", "ENABLED") {
if !setting.Cfg.Section("oauth").Key("ENABLED").MustBool() {
return
}
@@ -48,20 +48,21 @@ func NewOauthService() {
allOauthes := []string{"github", "google", "qq", "twitter", "weibo"}
// Load all OAuth config data.
for _, name := range allOauthes {
if !setting.Cfg.MustBool("oauth."+name, "ENABLED") {
sec := setting.Cfg.Section("oauth." + name)
if !sec.Key("ENABLED").MustBool() {
continue
}
setting.OauthService.OauthInfos[name] = &setting.OauthInfo{
Options: oauth2.Options{
ClientID: setting.Cfg.MustValue("oauth."+name, "CLIENT_ID"),
ClientSecret: setting.Cfg.MustValue("oauth."+name, "CLIENT_SECRET"),
Scopes: setting.Cfg.MustValueArray("oauth."+name, "SCOPES", " "),
ClientID: sec.Key("CLIENT_ID").String(),
ClientSecret: sec.Key("CLIENT_SECRET").String(),
Scopes: sec.Key("SCOPES").Strings(" "),
PathLogin: "/user/login/oauth2/" + name,
PathCallback: setting.AppSubUrl + "/user/login/" + name,
RedirectURL: setting.AppUrl + "user/login/" + name,
},
AuthUrl: setting.Cfg.MustValue("oauth."+name, "AUTH_URL"),
TokenUrl: setting.Cfg.MustValue("oauth."+name, "TOKEN_URL"),
AuthUrl: sec.Key("AUTH_URL").String(),
TokenUrl: sec.Key("TOKEN_URL").String(),
}
socialConfigs[name] = &oauth2.Options{
ClientID: setting.OauthService.OauthInfos[name].ClientID,
@@ -72,35 +73,35 @@ func NewOauthService() {
enabledOauths := make([]string, 0, 10)
// GitHub.
if setting.Cfg.MustBool("oauth.github", "ENABLED") {
if setting.Cfg.Section("oauth.github").Key("ENABLED").MustBool() {
setting.OauthService.GitHub = true
newGitHubOauth(socialConfigs["github"])
enabledOauths = append(enabledOauths, "GitHub")
}
// Google.
if setting.Cfg.MustBool("oauth.google", "ENABLED") {
if setting.Cfg.Section("oauth.google").Key("ENABLED").MustBool() {
setting.OauthService.Google = true
newGoogleOauth(socialConfigs["google"])
enabledOauths = append(enabledOauths, "Google")
}
// QQ.
if setting.Cfg.MustBool("oauth.qq", "ENABLED") {
if setting.Cfg.Section("oauth.qq").Key("ENABLED").MustBool() {
setting.OauthService.Tencent = true
newTencentOauth(socialConfigs["qq"])
enabledOauths = append(enabledOauths, "QQ")
}
// Twitter.
// if setting.Cfg.MustBool("oauth.twitter", "ENABLED") {
// if setting.Cfg.Section("oauth.twitter").Key( "ENABLED").MustBool() {
// setting.OauthService.Twitter = true
// newTwitterOauth(socialConfigs["twitter"])
// enabledOauths = append(enabledOauths, "Twitter")
// }
// Weibo.
if setting.Cfg.MustBool("oauth.weibo", "ENABLED") {
if setting.Cfg.Section("oauth.weibo").Key("ENABLED").MustBool() {
setting.OauthService.Weibo = true
newWeiboOauth(socialConfigs["weibo"])
enabledOauths = append(enabledOauths, "Weibo")

View File

@@ -1250,6 +1250,9 @@ The register and sign-in page style
color: #428BCA;
text-decoration: underline;
}
#repo-files-table td.message .text-truncate {
max-width: 360px;
}
#repo-files-table tbody {
background-color: #FFF;
}
@@ -2250,15 +2253,15 @@ textarea#issue-add-content {
#labels-num {
margin-right: 1em;
}
#label-list .right {
#label-list a.right {
margin-left: 1em;
color: #999;
line-height: 30px;
}
#label-list .right i {
#label-list a.right i {
margin-right: 3px;
}
#label-list .right:hover {
#label-list a.right:hover {
color: #444444;
}
#label-list .num {
@@ -2279,37 +2282,46 @@ textarea#issue-add-content {
margin-bottom: 12px;
border-bottom: 1px dashed #AAA;
}
#label-add-form {
padding: 18px 0;
border-bottom: 1px solid #DDD;
}
#label-add-form .ipt {
#label-add-form .ipt,
#label-edit-form .ipt,
#label-delete-form .ipt {
font-size: 14px;
}
#label-add-form .ipt[name=name] {
#label-add-form .ipt[name=name],
#label-edit-form .ipt[name=name],
#label-delete-form .ipt[name=name] {
width: 300px;
}
#label-add-form .btn {
#label-add-form .btn,
#label-edit-form .btn,
#label-delete-form .btn {
height: 33px;
font-size: 14px;
margin-left: 12px;
}
#label-color-drop .ipt {
#label-add-form {
padding: 18px 0;
border-bottom: 1px solid #DDD;
}
#label-delete-form span {
line-height: 33px;
}
.label-color-drop .ipt {
width: 100px;
}
#label-color-drop .drop-down {
width: 128px;
top: 22px;
left: 50px;
.label-color-drop .drop-down {
width: 128px !important;
top: 22px !important;
left: 50px !important;
padding: 12px;
line-height: 16px;
}
#label-color-drop .drop-down a.color {
.label-color-drop .drop-down a.color {
width: 16px;
height: 16px;
display: inline-block;
}
#label-color-drop label {
.label-color-drop label {
width: 24px;
height: 24px;
display: inline-block;

View File

@@ -214,7 +214,7 @@ var Gogs = {};
if (json.ok && json.data.length) {
var html = '';
$.each(json.data, function (i, item) {
html += '<li><a><img src="' + item.avatar_url + '">' + item.username;
html += '<li><a><img src="' + item.avatar_url + '"><span class="username">' + item.username + '</span>';
if (notEmpty(item.full_name)) {
html += ' (' + item.full_name + ')';
}
@@ -571,7 +571,7 @@ function initInvite() {
$ul.toggleShow();
}
}).next().next().find('ul').on("click", 'li', function () {
$('#org-member-invite').val($(this).text());
$('#org-member-invite').val($(this).find('.username').text());
$ul.toggleHide();
});
}

View File

@@ -14,22 +14,29 @@ $(document).ready(function(){
// render label color input
var color_input = $('#label-add-color');
var color_label = $('#label-color-drop label');
var color_label = $('#label-add-form .label-color-drop label');
color_label.css("background-color",labelColors[0]);
color_input.val(labelColors[0]);
// render label color drop
$('#label-color-drop .drop-down')
.html(colorDropHtml)
.on("click","a",function(){
var color = $(this).data("color-hex");
color_label.css("background-color",color);
color_input.val(color);
});
function render_color_drop($e){
$e.find('.label-color-drop .drop-down')
.html(colorDropHtml)
.on("click","a",function(){
var $form = $(this).parents(".form");
var color_label = $form.find(".label-color-drop label");
var color_input = $form.find("input[name=color]");
var color = $(this).data("color-hex");
color_label.css("background-color",color);
color_input.val(color);
});
}
// color drop visible
var form = $('#label-add-form');
render_color_drop(form);
$('#label-new-btn').on("click",function(){
if(form.hasClass("hidden")){
form.removeClass("hidden");
@@ -37,7 +44,54 @@ $(document).ready(function(){
});
$('#label-cancel-btn').on("click",function(){
form.addClass("hidden");
})
});
// label edit form render
var $edit_form_tpl = $("#label-edit-form-tpl");
$("#label-list").on("click","a.edit",function(){
var $label_item = $(this).parents(".item");
var $clone_form = $edit_form_tpl.clone();
render_color_drop($clone_form);
// add default color
var color_label = $clone_form.find(".label-color-drop label");
var color_input = $clone_form.find("input[name=color]");
var color = $label_item.find(".label").data("color-hex");
color_label.css("background-color",color);
color_input.val(color);
// add label name
$clone_form.find("input[name=name]").val($label_item.find(".label").text());
// add label id
$clone_form.find("input[name=id]").val($label_item.attr("id").replace("label-",""));
// append form
$label_item.after($clone_form.show());
// add cancel button event
$('#label-edit-cancel-btn').on("click",function(){
$clone_form.remove();
});
});
// label delete form render
var $del_form_tpl = $('#label-delete-form-tpl');
$("#label-list").on("click","a.delete",function(){
var $label_item = $(this).parents(".item");
var $clone_form = $del_form_tpl.clone();
// add label id
$clone_form.find("input[name=id]").val($label_item.attr("id").replace("label-",""));
// append form
$label_item.after($clone_form.show());
// add cancel button event
$('#label-del-cancel-btn').on("click",function(){
$clone_form.remove();
});
});
});

File diff suppressed because one or more lines are too long

View File

@@ -384,7 +384,7 @@ textarea#issue-add-content {
}
// labels list
#label-list {
.right {
a.right {
margin-left: 1em;
color: #999;
i {
@@ -416,10 +416,10 @@ textarea#issue-add-content {
border-bottom: 1px dashed #AAA;
}
}
// label add form
#label-add-form{
padding: 18px 0;
border-bottom: 1px solid #DDD;
// label add form, label edit form
#label-add-form,
#label-edit-form,
#label-delete-form{
.ipt{
font-size: 14px;
}
@@ -432,15 +432,24 @@ textarea#issue-add-content {
margin-left: 12px;
}
}
#label-add-form{
padding: 18px 0;
border-bottom: 1px solid #DDD;
}
#label-delete-form{
span{
line-height: 33px;
}
}
// label color drop
#label-color-drop{
.label-color-drop{
.ipt{
width:100px;
}
.drop-down{
width:128px;
top:22px;
left:50px;
width:128px !important;
top:22px !important;
left:50px !important;
padding: 12px;
line-height: 16px;
a.color{

View File

@@ -281,6 +281,11 @@
color: #428BCA;
text-decoration: underline;
}
td.message {
.text-truncate {
max-width: 360px;
}
}
tbody {
background-color: #FFF;
tr:hover {

View File

@@ -202,7 +202,6 @@ func Config(ctx *middleware.Context) {
ctx.Data["CacheInternal"] = setting.CacheInternal
ctx.Data["CacheConn"] = setting.CacheConn
ctx.Data["SessionProvider"] = setting.SessionProvider
ctx.Data["SessionConfig"] = setting.SessionConfig
ctx.Data["PictureService"] = setting.PictureService

View File

@@ -12,7 +12,6 @@ import (
"strings"
"github.com/Unknwon/com"
"github.com/Unknwon/goconfig"
"github.com/Unknwon/macaron"
"github.com/go-xorm/xorm"
@@ -32,7 +31,7 @@ const (
)
func checkRunMode() {
switch setting.Cfg.MustValue("", "RUN_MODE") {
switch setting.Cfg.Section("").Key("RUN_MODE").String() {
case "prod":
macaron.Env = macaron.PROD
setting.ProdMode = true
@@ -210,38 +209,40 @@ func InstallPost(ctx *middleware.Context, form auth.InstallForm) {
}
// Save settings.
setting.Cfg.SetValue("database", "DB_TYPE", models.DbCfg.Type)
setting.Cfg.SetValue("database", "HOST", models.DbCfg.Host)
setting.Cfg.SetValue("database", "NAME", models.DbCfg.Name)
setting.Cfg.SetValue("database", "USER", models.DbCfg.User)
setting.Cfg.SetValue("database", "PASSWD", models.DbCfg.Pwd)
setting.Cfg.SetValue("database", "SSL_MODE", models.DbCfg.SslMode)
setting.Cfg.SetValue("database", "PATH", models.DbCfg.Path)
setting.Cfg.Section("database").Key("DB_TYPE").SetValue(models.DbCfg.Type)
setting.Cfg.Section("database").Key("HOST").SetValue(models.DbCfg.Host)
setting.Cfg.Section("database").Key("NAME").SetValue(models.DbCfg.Name)
setting.Cfg.Section("database").Key("USER").SetValue(models.DbCfg.User)
setting.Cfg.Section("database").Key("PASSWD").SetValue(models.DbCfg.Pwd)
setting.Cfg.Section("database").Key("SSL_MODE").SetValue(models.DbCfg.SslMode)
setting.Cfg.Section("database").Key("PATH").SetValue(models.DbCfg.Path)
setting.Cfg.SetValue("repository", "ROOT", form.RepoRootPath)
setting.Cfg.SetValue("", "RUN_USER", form.RunUser)
setting.Cfg.SetValue("server", "DOMAIN", form.Domain)
setting.Cfg.SetValue("server", "ROOT_URL", form.AppUrl)
setting.Cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
setting.Cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
setting.Cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
setting.Cfg.Section("server").Key("ROOT_URL").SetValue(form.AppUrl)
if len(strings.TrimSpace(form.SmtpHost)) > 0 {
setting.Cfg.SetValue("mailer", "ENABLED", "true")
setting.Cfg.SetValue("mailer", "HOST", form.SmtpHost)
setting.Cfg.SetValue("mailer", "USER", form.SmtpEmail)
setting.Cfg.SetValue("mailer", "PASSWD", form.SmtpPasswd)
setting.Cfg.Section("mailer").Key("ENABLED").SetValue("true")
setting.Cfg.Section("mailer").Key("HOST").SetValue(form.SmtpHost)
setting.Cfg.Section("mailer").Key("USER").SetValue(form.SmtpEmail)
setting.Cfg.Section("mailer").Key("PASSWD").SetValue(form.SmtpPasswd)
setting.Cfg.SetValue("service", "REGISTER_EMAIL_CONFIRM", com.ToStr(form.RegisterConfirm == "on"))
setting.Cfg.SetValue("service", "ENABLE_NOTIFY_MAIL", com.ToStr(form.MailNotify == "on"))
setting.Cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(com.ToStr(form.RegisterConfirm == "on"))
setting.Cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(com.ToStr(form.MailNotify == "on"))
}
setting.Cfg.SetValue("", "RUN_MODE", "prod")
setting.Cfg.Section("").Key("RUN_MODE").SetValue("prod")
setting.Cfg.SetValue("log", "MODE", "file")
setting.Cfg.Section("session").Key("PROVIDER").SetValue("file")
setting.Cfg.SetValue("security", "INSTALL_LOCK", "true")
setting.Cfg.SetValue("security", "SECRET_KEY", base.GetRandomString(15))
setting.Cfg.Section("log").Key("MODE").SetValue("file")
setting.Cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
setting.Cfg.Section("security").Key("SECRET_KEY").SetValue(base.GetRandomString(15))
os.MkdirAll("custom/conf", os.ModePerm)
if err := goconfig.SaveConfigFile(setting.Cfg, path.Join(setting.CustomPath, "conf/app.ini")); err != nil {
if err := setting.Cfg.SaveTo(path.Join(setting.CustomPath, "conf/app.ini")); err != nil {
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), INSTALL, &form)
return
}

View File

@@ -208,7 +208,7 @@ func Diff(ctx *middleware.Context) {
commit := ctx.Repo.Commit
commit.CommitMessage = string(base.RenderIssueIndexPattern([]byte(commit.CommitMessage), ctx.Repo.RepoLink))
diff, err := models.GetDiffCommit(models.RepoPath(userName, repoName),
commitId, setting.MaxGitDiffLines)
commitId, setting.Git.MaxGitDiffLines)
if err != nil {
ctx.Handle(404, "GetDiffCommit", err)
return
@@ -272,7 +272,7 @@ func CompareDiff(ctx *middleware.Context) {
}
diff, err := models.GetDiffRange(models.RepoPath(userName, repoName), beforeCommitId,
afterCommitId, setting.MaxGitDiffLines)
afterCommitId, setting.Git.MaxGitDiffLines)
if err != nil {
ctx.Handle(404, "GetDiffRange", err)
return

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"strings"
"time"
"path"
"github.com/Unknwon/com"
@@ -169,7 +170,7 @@ func SettingsCollaboration(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsCollaboration"] = true
repoLink := strings.TrimPrefix(ctx.Repo.RepoLink, "/")
repoLink := path.Join(ctx.Repo.Owner.LowerName, ctx.Repo.Repository.LowerName)
if ctx.Req.Method == "POST" {
name := strings.ToLower(ctx.Query("collaborator"))

View File

@@ -98,7 +98,7 @@ func Home(ctx *middleware.Context) {
readmeExist := base.IsMarkdownFile(blob.Name()) || base.IsReadmeFile(blob.Name())
ctx.Data["ReadmeExist"] = readmeExist
if readmeExist {
ctx.Data["FileContent"] = string(base.RenderMarkdown(buf, ""))
ctx.Data["FileContent"] = string(base.RenderMarkdown(buf, branchLink))
} else {
if err, content := base.ToUtf8WithErr(buf); err != nil {
if err != nil {
@@ -154,8 +154,12 @@ func Home(ctx *middleware.Context) {
// Render issue index links.
for _, f := range files {
c := f[1].(*git.Commit)
c.CommitMessage = string(base.RenderIssueIndexPattern([]byte(c.CommitMessage), ctx.Repo.RepoLink))
switch c := f[1].(type) {
case *git.Commit:
c.CommitMessage = string(base.RenderIssueIndexPattern([]byte(c.CommitMessage), ctx.Repo.RepoLink))
case *git.SubModuleFile:
c.CommitMessage = string(base.RenderIssueIndexPattern([]byte(c.CommitMessage), ctx.Repo.RepoLink))
}
}
ctx.Data["Files"] = files

View File

@@ -62,6 +62,8 @@ func SignIn(ctx *middleware.Context) {
if err != nil {
if err != models.ErrUserNotExist {
ctx.Handle(500, "GetUserByName", err)
} else {
ctx.HTML(200, SIGNIN)
}
return
}
@@ -343,6 +345,27 @@ func Activate(ctx *middleware.Context) {
ctx.HTML(200, ACTIVATE)
}
func ActivateEmail(ctx *middleware.Context) {
code := ctx.Query("code")
email_string := ctx.Query("email")
// Verify code.
if email := models.VerifyActiveEmailCode(code, email_string); email != nil {
err := email.Activate()
if err != nil {
ctx.Handle(500, "ActivateEmail", err)
}
log.Trace("Email activated: %s", email.Email)
ctx.Flash.Success(ctx.Tr("settings.activate_email_success"))
}
ctx.Redirect(setting.AppSubUrl + "/user/settings/email")
return
}
func ForgotPasswd(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password")

View File

@@ -105,6 +105,9 @@ func Dashboard(ctx *middleware.Context) {
// FIXME: cache results?
u, err := models.GetUserByName(act.ActUserName)
if err != nil {
if err == models.ErrUserNotExist {
continue
}
ctx.Handle(500, "GetUserByName", err)
return
}
@@ -198,9 +201,21 @@ func Profile(ctx *middleware.Context) {
}
feeds := make([]*models.Action, 0, len(actions))
for _, act := range actions {
if act.IsPrivate {
if !ctx.IsSigned {
continue
}
if has, _ := models.HasAccess(ctx.User.Name, act.RepoUserName+"/"+act.RepoName,
models.READABLE); !has {
continue
}
}
// FIXME: cache results?
u, err := models.GetUserByName(act.ActUserName)
if err != nil {
if err == models.ErrUserNotExist {
continue
}
ctx.Handle(500, "GetUserByName", err)
return
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/gogits/gogs/modules/auth"
"github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/mailer"
"github.com/gogits/gogs/modules/middleware"
"github.com/gogits/gogs/modules/setting"
)
@@ -21,6 +22,7 @@ import (
const (
SETTINGS_PROFILE base.TplName = "user/settings/profile"
SETTINGS_PASSWORD base.TplName = "user/settings/password"
SETTINGS_EMAILS base.TplName = "user/settings/email"
SETTINGS_SSH_KEYS base.TplName = "user/settings/sshkeys"
SETTINGS_SOCIAL base.TplName = "user/settings/social"
SETTINGS_APPLICATIONS base.TplName = "user/settings/applications"
@@ -126,6 +128,112 @@ func SettingsAvatar(ctx *middleware.Context, form auth.UploadAvatarForm) {
ctx.Flash.Success(ctx.Tr("settings.update_avatar_success"))
}
func SettingsEmails(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsUserSettings"] = true
ctx.Data["PageIsSettingsEmails"] = true
var err error
ctx.Data["Emails"], err = models.GetEmailAddresses(ctx.User.Id)
if err != nil {
ctx.Handle(500, "email.GetEmailAddresses", err)
return
}
ctx.HTML(200, SETTINGS_EMAILS)
}
func SettingsEmailPost(ctx *middleware.Context, form auth.AddEmailForm) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsUserSettings"] = true
ctx.Data["PageIsSettingsEmails"] = true
var err error
ctx.Data["Emails"], err = models.GetEmailAddresses(ctx.User.Id)
if err != nil {
ctx.Handle(500, "email.GetEmailAddresses", err)
return
}
// Delete Email address.
if ctx.Query("_method") == "DELETE" {
id := com.StrTo(ctx.Query("id")).MustInt64()
if id <= 0 {
return
}
if err = models.DeleteEmailAddress(&models.EmailAddress{Id: id}); err != nil {
ctx.Handle(500, "DeleteEmail", err)
} else {
log.Trace("Email address deleted: %s", ctx.User.Name)
ctx.Redirect(setting.AppSubUrl + "/user/settings/email")
}
return
}
// Make emailaddress primary.
if ctx.Query("_method") == "PRIMARY" {
id := com.StrTo(ctx.Query("id")).MustInt64()
if id <= 0 {
return
}
if err = models.MakeEmailPrimary(&models.EmailAddress{Id: id}); err != nil {
ctx.Handle(500, "MakeEmailPrimary", err)
} else {
log.Trace("Email made primary: %s", ctx.User.Name)
ctx.Redirect(setting.AppSubUrl + "/user/settings/email")
}
return
}
// Add Email address.
if ctx.Req.Method == "POST" {
if ctx.HasError() {
ctx.HTML(200, SETTINGS_EMAILS)
return
}
cleanEmail := strings.Replace(form.Email, "\n", "", -1)
e := &models.EmailAddress{
Uid: ctx.User.Id,
Email: cleanEmail,
IsActivated: !setting.Service.RegisterEmailConfirm,
}
if err := models.AddEmailAddress(e); err != nil {
if err == models.ErrEmailAlreadyUsed {
ctx.RenderWithErr(ctx.Tr("form.email_has_been_used"), SETTINGS_EMAILS, &form)
return
}
ctx.Handle(500, "email.AddEmailAddress", err)
return
} else {
// Send confirmation e-mail
if setting.Service.RegisterEmailConfirm {
mailer.SendActivateEmail(ctx.Render, ctx.User, e)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
}
ctx.Flash.Success(ctx.Tr("settings.add_email_success_confirmation_email_sent"))
} else {
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
}
log.Trace("Email address added: %s", e.Email)
ctx.Redirect(setting.AppSubUrl + "/user/settings/email")
return
}
}
ctx.HTML(200, SETTINGS_EMAILS)
}
func SettingsPassword(ctx *middleware.Context) {
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsUserSettings"] = true

View File

@@ -1,21 +1,25 @@
rm -rf output
mkdir output
outPath=./output
rm -rf $outPath
mkdir $outPath
go build ../gogs.go
chmod +x gogs
mv gogs ./output/
cp -r ../conf/ ./output/conf/
cp -r ../custom/ ./output/custom/
cp -r ./dockerfiles/ ./output/dockerfiles/
cp -r ../public/ ./output/public/
cp -r ../templates/ ./output/templates/
cp ../cert.pem ./output/
cp ../CONTRIBUTING.md ./output/
cp gogs_supervisord.sh ./output/
cp ../key.pem ./output/
cp ../LICENSE ./output/
cp ../README.md ./output/
cp ../README_ZH.md ./output/
cp start.bat ./output/
cp start.sh ./output/
cp ../wercker.yml ./output/
cp mysql.sql ./output/
mv gogs $outPath/
cp -r ../conf/ $outPath/conf/
cp -r ../custom/ $outPath/custom/
cp -r dockerfiles/ $outPath/dockerfiles/
cp -r ../public/ $outPath/public/
cp -r ../templates/ $outPath/templates/
cp ../cert.pem $outPath/
cp ../CONTRIBUTING.md $outPath/
cp gogs_supervisord.sh $outPath/
cp ../key.pem $outPath/
cp ../LICENSE $outPath/
cp ../README.md $outPath/
cp ../README_ZH.md $outPath/
cp start.bat $outPath/
cp start.sh $outPath/
cp ../wercker.yml $outPath/
cp mysql.sql $outPath/

27
scripts/build_freebsd.sh Executable file
View File

@@ -0,0 +1,27 @@
outPlattform=freebsd
outArch=amd64
outPath=./output_$outPlattform_$outArch
rm -rf $outPath
mkdir $outPath
CGO_ENABLED=0 GOOS=$outPlattform GOARCH=$outArch go build ../gogs.go
chmod +x gogs
mv gogs $outPath/
cp -r ../conf/ $outPath/conf/
cp -r ../custom/ $outPath/custom/
cp -r dockerfiles/ $outPath/dockerfiles/
cp -r ../public/ $outPath/public/
cp -r ../templates/ $outPath/templates/
cp ../cert.pem $outPath/
cp ../CONTRIBUTING.md $outPath/
cp gogs_supervisord.sh $outPath/
cp ../key.pem $outPath/
cp ../LICENSE $outPath/
cp ../README.md $outPath/
cp ../README_ZH.md $outPath/
cp start.bat $outPath/
cp start.sh $outPath/
cp ../wercker.yml $outPath/
cp mysql.sql $outPath/

View File

@@ -1,21 +1,27 @@
rm -rf output_linux_64
mkdir output_linux_64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ../gogs.go
outPlattform=linux
outArch=amd64
outPath=./output_$outPlattform_$outArch
rm -rf $outPath
mkdir $outPath
CGO_ENABLED=0 GOOS=$outPlattform GOARCH=$outArch go build ../gogs.go
chmod +x gogs
mv gogs ./output_linux_64/
cp -r ../conf/ ./output_linux_64/conf/
cp -r ../custom/ ./output_linux_64/custom/
cp -r dockerfiles/ ./output_linux_64/dockerfiles/
cp -r ../public/ ./output_linux_64/public/
cp -r ../templates/ ./output_linux_64/templates/
cp ../cert.pem ./output_linux_64/
cp ../CONTRIBUTING.md ./output_linux_64/
cp gogs_supervisord.sh ./output_linux_64/
cp ../key.pem ./output_linux_64/
cp ../LICENSE ./output_linux_64/
cp ../README.md ./output_linux_64/
cp ../README_ZH.md ./output_linux_64/
cp start.bat ./output_linux_64/
cp start.sh ./output_linux_64/
cp ../wercker.yml ./output_linux_64/
cp mysql.sql ./output_linux_64/
mv gogs $outPath/
cp -r ../conf/ $outPath/conf/
cp -r ../custom/ $outPath/custom/
cp -r dockerfiles/ $outPath/dockerfiles/
cp -r ../public/ $outPath/public/
cp -r ../templates/ $outPath/templates/
cp ../cert.pem $outPath/
cp ../CONTRIBUTING.md $outPath/
cp gogs_supervisord.sh $outPath/
cp ../key.pem $outPath/
cp ../LICENSE $outPath/
cp ../README.md $outPath/
cp ../README_ZH.md $outPath/
cp start.bat $outPath/
cp start.sh $outPath/
cp ../wercker.yml $outPath/
cp mysql.sql $outPath/

View File

@@ -1 +1 @@
0.5.9.1213 Beta
0.5.11.0103 Beta

View File

@@ -34,8 +34,8 @@
<td><a href="{{AppSubUrl}}/admin/auths/{{.Id}}">{{.Name}}</a></td>
<td>{{.TypeString}}</td>
<td><i class="fa fa{{if .IsActived}}-check{{end}}-square-o"></i></td>
<td>{{DateFormat .Updated "M d, Y"}}</td>
<td>{{DateFormat .Created "M d, Y"}}</td>
<td><span title="{{DateFormat .Updated "r"}}">{{DateFormat .Updated "M d, Y"}}</span></td>
<td><span title="{{DateFormat .Created "r"}}">{{DateFormat .Created "M d, Y"}}</span></td>
<td><a href="{{AppSubUrl}}/admin/auths/{{.Id}}"><i class="fa fa-pencil-square-o"></i></a></td>
</tr>
{{end}}

View File

@@ -169,13 +169,11 @@
<div class="panel-body">
<dl class="dl-horizontal admin-dl-horizontal">
<dt>{{.i18n.Tr "admin.config.session_provider"}}</dt>
<dd>{{.SessionProvider}}</dd>
<dd>{{.SessionConfig.Provider}}</dd>
<dt>{{.i18n.Tr "admin.config.provider_config"}}</dt>
<dd><pre>{{.SessionConfig.ProviderConfig}}</pre></dd>
<dt>{{.i18n.Tr "admin.config.cookie_name"}}</dt>
<dd>{{.SessionConfig.CookieName}}</dd>
<dt>{{.i18n.Tr "admin.config.enable_set_cookie"}}</dt>
<dd><i class="fa fa{{if .SessionConfig.EnableSetCookie}}-check{{end}}-square-o"></i></dd>
<dt>{{.i18n.Tr "admin.config.gc_interval_time"}}</dt>
<dd>{{.SessionConfig.Gclifetime}} {{.i18n.Tr "tool.raw_seconds"}}</dd>
<dt>{{.i18n.Tr "admin.config.session_life_time"}}</dt>

View File

@@ -35,7 +35,7 @@
<td>{{.NumTeams}}</td>
<td>{{.NumMembers}}</td>
<td>{{.NumRepos}}</td>
<td>{{DateFormat .Created "M d, Y"}}</td>
<td><span title="{{DateFormat .Created "r"}}">{{DateFormat .Created "M d, Y"}}</span></td>
</tr>
{{end}}
</tbody>

View File

@@ -37,7 +37,7 @@
<td>{{.NumWatches}}</td>
<td>{{.NumStars}}</td>
<td>{{.NumIssues}}</td>
<td>{{DateFormat .Created "M d, Y"}}</td>
<td><span title="{{DateFormat .Created "r"}}">{{DateFormat .Created "M d, Y"}}</span></td>
</tr>
{{end}}
</tbody>

View File

@@ -37,7 +37,7 @@
<td><i class="fa fa{{if .IsActive}}-check{{end}}-square-o"></i></td>
<td><i class="fa fa{{if .IsAdmin}}-check{{end}}-square-o"></i></td>
<td>{{.NumRepos}}</td>
<td>{{DateFormat .Created "M d, Y"}}</td>
<td><span title="{{DateFormat .Created "r"}}">{{DateFormat .Created "M d, Y"}}</span></td>
<td><a href="{{AppSubUrl}}/admin/users/{{.Id}}"><i class="fa fa-pencil-square-o"></i></a></td>
</tr>
{{end}}

View File

@@ -0,0 +1,30 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{{.User.Name}}, please activate your e-mail address</title>
</head>
<body style="background:#eee;">
<div style="color:#333; font:12px/1.5 Tahoma,Arial,sans-serif;; text-shadow:1px 1px #fff; padding:0; margin:0;">
<div style="width:600px;margin:0 auto; padding:40px 0 20px;">
<div style="border:1px solid #d9d9d9;border-radius:3px; background:#fff; box-shadow: 0px 2px 5px rgba(0, 0, 0,.05); -webkit-box-shadow: 0px 2px 5px rgba(0, 0, 0,.05);">
<div style="padding: 20px 15px;">
<h1 style="font-size:20px; padding:10px 0 20px; margin:0; border-bottom:1px solid #ddd;"><img src="{{.AppUrl}}/img/favicon.png" style="height: 32px; margin-bottom: -10px;"> <a style="color:#333;text-decoration:none;" target="_blank" href="{{.AppUrl}}">{{.AppName}}</a></h1>
<div style="padding:40px 15px;">
<div style="font-size:16px; padding-bottom:30px; font-weight:bold;">
Hi <span style="color: #00BFFF;">{{.User.Name}}</span>,
</div>
<div style="font-size:14px; padding:0 15px;">
<p style="margin:0;padding:0 0 9px 0;">Please click the following link to verify your e-mail address within <b>{{.ActiveCodeLives}} hours</b>.</p>
<p style="margin:0;padding:0 0 9px 0;">
<a href="{{.AppUrl}}user/activate_email?code={{.Code}}&email={{.Email}}">{{.AppUrl}}user/activate_email?code={{.Code}}&email={{.Email}}</a>
</p>
<p style="margin:0;padding:0 0 9px 0;">Not working? Try copying and pasting it to your browser.</p>
</div>
</div>
</div>
</div>
<div style="color:#aaa;padding:10px;text-align:center;">
© 2014 <a style="color:#888;text-decoration:none;" target="_blank" href="http://gogits.org">Gogs: Go Git Service</a>
</div>
</div>
</div>
</body>
</html>

View File

@@ -12,7 +12,7 @@
</ul>
<form id="label-add-form" action="#" class="form clear hidden">
<input type="text" class="ipt" name="name" placeholder="label name" id="label-add-name"/>
<div class="inline down drop" id="label-color-drop">
<div class="inline down drop label-color-drop">
<label for="label-add-color"></label>
<input class="ipt" name="color" type="text" placeholder="color" id="label-add-color"/>
<div class="drop-down">
@@ -30,10 +30,10 @@
</div>
<ul id="label-list" class="list-no-style">
<li class="item" id="label-id">
<a class="right" href="#"><i class="octicon octicon-x"></i>Delete</a>
<a class="right" href="#"><i class="octicon octicon-pencil"></i>Edit</a>
<a class="right delete" href="#"><i class="octicon octicon-x"></i>Delete</a>
<a class="right edit" href="#"><i class="octicon octicon-pencil"></i>Edit</a>
<a class="right issue-num" href="#"><i class="octicon octicon-issue-opened"></i><strong class="num">12</strong>Issues</a>
<a class="left label clear" href="#" style="background-color: red"><i class="octicon octicon-tag"></i><strong>bug</strong></a>
<a class="left label clear" href="#" style="background-color: #0052cc" data-color-hex="#0052cc"><i class="octicon octicon-tag"></i><strong>bug</strong></a>
</li>
<li class="item" id="label-id">
<a class="right" href="#"><i class="octicon octicon-x"></i>Delete</a>
@@ -45,4 +45,30 @@
</div>
</div>
</div>
<div id="label-edit-form-tpl" class="hidden">
<li class="item"><form id="label-edit-form" action="#" class="form clear">
<input type="text" class="ipt" name="name" placeholder="label name" id="label-edit-name"/>
<input type="hidden" name="id" value="id"/>
<div class="inline down drop label-color-drop">
<label for="label-add-color"></label>
<input class="ipt" name="color" type="text" placeholder="color" id="label-edit-color"/>
<div class="drop-down">
<a href="#" class="color" style="background: red"></a>
<a href="#" class="color" style="background: green"></a>
</div>
</div>
<button class="btn btn-gray right" type="button" id="label-edit-cancel-btn">Cancel</button>
<button class="btn btn-green right" id="label-edit-btn">Save Changes</button>
</form></li>
</div>
<div id="label-delete-form-tpl" class="hidden">
<li class="item">
<form id="label-delete-form" action="#">
<input type="hidden" name="id" value="id"/>
<span><strong class="text-red">Are you sure?</strong> Deleting a label will remove it from all issues and pull requests.</span>
<button class="btn btn-gray right" type="button" id="label-del-cancel-btn">Cancel</button>
<button class="btn btn-red right" id="label-del-btn">Delete</button>
</form>
</li>
</div>
{{template "ng/base/footer" .}}

View File

@@ -22,7 +22,7 @@
<i class="fa fa-send fa-2x left"></i>
<div class="ssh-content left">
<p><strong>{{.Name}}</strong></p>
<p class="activity"><i>{{$.i18n.Tr "settings.add_on"}} {{DateFormat .Created "M d, Y"}} — <i class="octicon octicon-info"></i>{{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} {{DateFormat .Updated "M d, Y"}}{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i></p>
<p class="activity"><i>{{$.i18n.Tr "settings.add_on"}} <span title="{{DateFormat .Created "r"}}">{{DateFormat .Created "M d, Y"}}</span> — <i class="octicon octicon-info"></i>{{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} {{DateFormat .Updated "M d, Y"}}{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i></p>
</div>
<a href="{{AppSubUrl}}/user/settings/applications?remove={{.Id}}">
<button class="btn btn-small btn-red btn-radius ssh-btn right">{{$.i18n.Tr "settings.delete_token"}}</button>

View File

@@ -0,0 +1,58 @@
{{template "ng/base/head" .}}
{{template "ng/base/header" .}}
<div id="setting-wrapper" class="main-wrapper">
<div id="user-profile-setting" class="container clear">
{{template "user/settings/nav" .}}
<div class="grid-4-5 left">
<div class="setting-content">
{{template "ng/base/alert" .}}
<div id="user-email-setting-content">
<div id="user-email-panel" class="panel panel-radius">
<div class="panel-header">
<strong>{{.i18n.Tr "settings.manage_emails"}}</strong>
</div>
<ul class="panel-body setting-list">
<li>{{.i18n.Tr "settings.email_desc"}}</li>
{{range .Emails}}
<li class="email clear">
<div class="email-content left">
<p><strong>{{.Email}}</strong> {{if .IsPrimary}} <span class="email-primary">{{$.i18n.Tr "settings.primary"}}</span> {{end}}</p>
</div>
{{if not .IsPrimary}}
{{if .IsActivated}}
<form action="{{AppSubUrl}}/user/settings/email" method="post">
{{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="PRIMARY">
<input name="id" type="hidden" value="{{.Id}}">
<button class="right email-btn btn btn-green btn-radius btn-small">{{$.i18n.Tr "settings.primary_email"}}</button>
</form>
{{end}}
<form action="{{AppSubUrl}}/user/settings/email" method="post">
{{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="DELETE">
<input name="id" type="hidden" value="{{.Id}}">
<button class="right email-btn btn btn-red btn-radius btn-small">{{$.i18n.Tr "settings.delete_email"}}</button>
</form>
{{end}}
</li>
{{end}}
<form action="{{AppSubUrl}}/user/settings/email" method="post">
{{.CsrfTokenHtml}}
<p class="panel-header"><strong>{{.i18n.Tr "settings.add_new_email"}}</strong></p>
<p class="field">
<label class="req" for="email">{{.i18n.Tr "email"}}</label>
<input class="ipt ipt-radius" id="email" name="email" type="text" required />
</p>
<p class="field">
<label></label>
<button class="btn btn-green btn-radius" id="email-add-btn">{{.i18n.Tr "settings.add_email"}}</button>
</p>
</form>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{{template "ng/base/footer" .}}

View File

@@ -4,6 +4,7 @@
<ul class="menu menu-vertical switching-list grid-1-5 left">
<li {{if .PageIsSettingsProfile}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings">{{.i18n.Tr "settings.profile"}}</a></li>
<li {{if .PageIsSettingsPassword}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/password">{{.i18n.Tr "settings.password"}}</a></li>
<li {{if .PageIsSettingsEmail}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/email">{{.i18n.Tr "settings.emails"}}</a></li>
<li {{if .PageIsSettingsSSHKeys}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/ssh">{{.i18n.Tr "settings.ssh_keys"}}</a></li>
<li {{if .PageIsSettingsSocial}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/social">{{.i18n.Tr "settings.social"}}</a></li>
<li {{if .PageIsSettingsApplications}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/applications">{{.i18n.Tr "settings.applications"}}</a></li>

View File

@@ -18,7 +18,7 @@
<div class="ssh-content left">
<p><strong>{{Oauth2Name .Type}}</strong></p>
<p class="print">{{.Identity}}</p>
<p class="activity"><i>{{$.i18n.Tr "settings.add_on"}} {{DateFormat .Created "M d, Y"}} — <i class="octicon octicon-info"></i>{{$.i18n.Tr "settings.last_used"}} {{DateFormat .Updated "M d, Y"}}</i></p>
<p class="activity"><i>{{$.i18n.Tr "settings.add_on"}} <span title="{{DateFormat .Created "r"}}">{{DateFormat .Created "M d, Y"}}</span> — <i class="octicon octicon-info"></i>{{$.i18n.Tr "settings.last_used"}} {{DateFormat .Updated "M d, Y"}}</i></p>
</div>
<a class="right btn btn-small btn-red btn-header btn-radius" href="{{AppSubUrl}}/user/settings/social?remove={{.Id}}">{{$.i18n.Tr "settings.unbind"}}</a>
</li>

View File

@@ -23,7 +23,7 @@
<div class="ssh-content left">
<p><strong>{{.Name}}</strong></p>
<p class="print">{{.Fingerprint}}</p>
<p class="activity"><i>{{$.i18n.Tr "settings.add_on"}} {{DateFormat .Created "M d, Y"}} — <i class="octicon octicon-info"></i>{{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} {{DateFormat .Updated "M d, Y"}}{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i></p>
<p class="activity"><i>{{$.i18n.Tr "settings.add_on"}} <span title="{{DateFormat .Created "r"}}">{{DateFormat .Created "M d, Y"}}</span> — <i class="octicon octicon-info"></i>{{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} <span title="{{DateFormat .Updated "r"}}">{{DateFormat .Updated "M d, Y"}}</span>{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i></p>
</div>
<form action="{{AppSubUrl}}/user/settings/ssh" method="post">
{{$.CsrfTokenHtml}}