# Conflicts:
#	options/locale/locale_en-US.ini
This commit is contained in:
kolaente 2018-04-29 11:25:56 +02:00
commit c0cd0c3710
No known key found for this signature in database
GPG Key ID: F40E70337AB24C9B
47 changed files with 2238 additions and 213 deletions

View File

@ -28,6 +28,8 @@ for SOURCE in $(find ${ROOT}/content -type f -iname *.en-us.md); do
if [[ ! -f ${DEST} ]]; then
echo "Creating fallback for ${DEST#${ROOT}/content/}"
cp ${SOURCE} ${DEST}
sed -i.bak "s/en\-us/${LOCALE}/g" ${DEST}
rm ${DEST}.bak
fi
done
done

View File

@ -3,3 +3,4 @@
repo_id: 1
hook_id: 1
uuid: uuid1
is_delivered: true

View File

@ -51,9 +51,10 @@ type Issue struct {
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
ClosedUnix util.TimeStamp `xorm:"INDEX"`
Attachments []*Attachment `xorm:"-"`
Comments []*Comment `xorm:"-"`
Reactions ReactionList `xorm:"-"`
Attachments []*Attachment `xorm:"-"`
Comments []*Comment `xorm:"-"`
Reactions ReactionList `xorm:"-"`
TotalTrackedTime int64 `xorm:"-"`
}
var (
@ -69,6 +70,15 @@ func init() {
issueTasksDonePat = regexp.MustCompile(issueTasksDoneRegexpStr)
}
func (issue *Issue) loadTotalTimes(e Engine) (err error) {
opts := FindTrackedTimesOptions{IssueID: issue.ID}
issue.TotalTrackedTime, err = opts.ToSession(e).SumInt(&TrackedTime{}, "time")
if err != nil {
return err
}
return nil
}
func (issue *Issue) loadRepo(e Engine) (err error) {
if issue.Repo == nil {
issue.Repo, err = getRepositoryByID(e, issue.RepoID)
@ -79,6 +89,15 @@ func (issue *Issue) loadRepo(e Engine) (err error) {
return nil
}
// IsTimetrackerEnabled returns true if the repo enables timetracking
func (issue *Issue) IsTimetrackerEnabled() bool {
if err := issue.loadRepo(x); err != nil {
log.Error(4, fmt.Sprintf("loadRepo: %v", err))
return false
}
return issue.Repo.IsTimetrackerEnabled()
}
// GetPullRequest returns the issue pull request
func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
if !issue.IsPull {
@ -225,6 +244,11 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
if err = issue.loadComments(e); err != nil {
return err
}
if issue.IsTimetrackerEnabled() {
if err = issue.loadTotalTimes(e); err != nil {
return err
}
}
return issue.loadReactions(e)
}

View File

@ -441,7 +441,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
}
// update the issue's updated_unix column
if err = updateIssueCols(e, opts.Issue); err != nil {
if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil {
return nil, err
}

View File

@ -290,6 +290,50 @@ func (issues IssueList) loadComments(e Engine) (err error) {
return nil
}
func (issues IssueList) loadTotalTrackedTimes(e Engine) (err error) {
type totalTimesByIssue struct {
IssueID int64
Time int64
}
if len(issues) == 0 {
return nil
}
var trackedTimes = make(map[int64]int64, len(issues))
var ids = make([]int64, 0, len(issues))
for _, issue := range issues {
if issue.Repo.IsTimetrackerEnabled() {
ids = append(ids, issue.ID)
}
}
// select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
rows, err := e.Table("tracked_time").
Select("issue_id, sum(time) as time").
In("issue_id", ids).
GroupBy("issue_id").
Rows(new(totalTimesByIssue))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var totalTime totalTimesByIssue
err = rows.Scan(&totalTime)
if err != nil {
return err
}
trackedTimes[totalTime.IssueID] = totalTime.Time
}
for _, issue := range issues {
issue.TotalTrackedTime = trackedTimes[issue.ID]
}
return nil
}
// loadAttributes loads all attributes, expect for attachments and comments
func (issues IssueList) loadAttributes(e Engine) (err error) {
if _, err = issues.loadRepositories(e); err != nil {
@ -316,6 +360,10 @@ func (issues IssueList) loadAttributes(e Engine) (err error) {
return
}
if err = issues.loadTotalTrackedTimes(e); err != nil {
return
}
return nil
}

View File

@ -7,6 +7,8 @@ package models
import (
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
@ -29,7 +31,7 @@ func TestIssueList_LoadRepositories(t *testing.T) {
func TestIssueList_LoadAttributes(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
setting.Service.EnableTimetracking = true
issueList := IssueList{
AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
@ -61,5 +63,10 @@ func TestIssueList_LoadAttributes(t *testing.T) {
for _, comment := range issue.Comments {
assert.EqualValues(t, issue.ID, comment.IssueID)
}
if issue.ID == int64(1) {
assert.Equal(t, int64(400), issue.TotalTrackedTime)
} else if issue.ID == int64(2) {
assert.Equal(t, int64(3662), issue.TotalTrackedTime)
}
}
}

View File

@ -29,6 +29,8 @@ type Milestone struct {
DeadlineString string `xorm:"-"`
DeadlineUnix util.TimeStamp
ClosedDateUnix util.TimeStamp
TotalTrackedTime int64 `xorm:"-"`
}
// BeforeUpdate is invoked from XORM before updating this object.
@ -118,14 +120,69 @@ func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
return getMilestoneByRepoID(x, repoID, id)
}
// MilestoneList is a list of milestones offering additional functionality
type MilestoneList []*Milestone
func (milestones MilestoneList) loadTotalTrackedTimes(e Engine) error {
type totalTimesByMilestone struct {
MilestoneID int64
Time int64
}
if len(milestones) == 0 {
return nil
}
var trackedTimes = make(map[int64]int64, len(milestones))
// Get total tracked time by milestone_id
rows, err := e.Table("issue").
Join("INNER", "milestone", "issue.milestone_id = milestone.id").
Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
Select("milestone_id, sum(time) as time").
In("milestone_id", milestones.getMilestoneIDs()).
GroupBy("milestone_id").
Rows(new(totalTimesByMilestone))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var totalTime totalTimesByMilestone
err = rows.Scan(&totalTime)
if err != nil {
return err
}
trackedTimes[totalTime.MilestoneID] = totalTime.Time
}
for _, milestone := range milestones {
milestone.TotalTrackedTime = trackedTimes[milestone.ID]
}
return nil
}
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
func (milestones MilestoneList) LoadTotalTrackedTimes() error {
return milestones.loadTotalTrackedTimes(x)
}
func (milestones MilestoneList) getMilestoneIDs() []int64 {
var ids = make([]int64, 0, len(milestones))
for _, ms := range milestones {
ids = append(ids, ms.ID)
}
return ids
}
// GetMilestonesByRepoID returns all milestones of a repository.
func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
func GetMilestonesByRepoID(repoID int64) (MilestoneList, error) {
miles := make([]*Milestone, 0, 10)
return miles, x.Where("repo_id = ?", repoID).Find(&miles)
}
// GetMilestones returns a list of milestones of given repository and status.
func GetMilestones(repoID int64, page int, isClosed bool, sortType string) ([]*Milestone, error) {
func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
if page > 0 {
@ -146,7 +203,6 @@ func GetMilestones(repoID int64, page int, isClosed bool, sortType string) ([]*M
default:
sess.Asc("deadline_unix")
}
return miles, sess.Find(&miles)
}

View File

@ -253,3 +253,14 @@ func TestDeleteMilestoneByRepoID(t *testing.T) {
assert.NoError(t, DeleteMilestoneByRepoID(NonexistentID, NonexistentID))
}
func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
miles := MilestoneList{
AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone),
}
assert.NoError(t, miles.LoadTotalTrackedTimes())
assert.Equal(t, miles[0].TotalTrackedTime, int64(3662))
}

View File

@ -69,7 +69,7 @@ func CreateOrStopIssueStopwatch(user *User, issue *Issue) error {
Doer: user,
Issue: issue,
Repo: issue.Repo,
Content: secToTime(timediff),
Content: SecToTime(timediff),
Type: CommentTypeStopTracking,
}); err != nil {
return err
@ -124,7 +124,8 @@ func CancelStopwatch(user *User, issue *Issue) error {
return nil
}
func secToTime(duration int64) string {
// SecToTime converts an amount of seconds to a human-readable string (example: 66s -> 1min 6s)
func SecToTime(duration int64) string {
seconds := duration % 60
minutes := (duration / (60)) % 60
hours := duration / (60 * 60)

View File

@ -279,3 +279,11 @@ func TestGetUserIssueStats(t *testing.T) {
assert.Equal(t, test.ExpectedIssueStats, *stats)
}
}
func TestIssue_loadTotalTimes(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
ms, err := GetIssueByID(2)
assert.NoError(t, err)
assert.NoError(t, ms.loadTotalTimes(x))
assert.Equal(t, int64(3662), ms.TotalTrackedTime)
}

View File

@ -11,6 +11,7 @@ import (
api "code.gitea.io/sdk/gitea"
"github.com/go-xorm/builder"
"github.com/go-xorm/xorm"
)
// TrackedTime represents a time that was spent for a specific issue.
@ -44,6 +45,7 @@ type FindTrackedTimesOptions struct {
IssueID int64
UserID int64
RepositoryID int64
MilestoneID int64
}
// ToCond will convert each condition into a xorm-Cond
@ -58,16 +60,23 @@ func (opts *FindTrackedTimesOptions) ToCond() builder.Cond {
if opts.RepositoryID != 0 {
cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID})
}
if opts.MilestoneID != 0 {
cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID})
}
return cond
}
// ToSession will convert the given options to a xorm Session by using the conditions from ToCond and joining with issue table if required
func (opts *FindTrackedTimesOptions) ToSession(e Engine) *xorm.Session {
if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
return e.Join("INNER", "issue", "issue.id = tracked_time.issue_id").Where(opts.ToCond())
}
return x.Where(opts.ToCond())
}
// GetTrackedTimes returns all tracked times that fit to the given options.
func GetTrackedTimes(options FindTrackedTimesOptions) (trackedTimes []*TrackedTime, err error) {
if options.RepositoryID > 0 {
err = x.Join("INNER", "issue", "issue.id = tracked_time.issue_id").Where(options.ToCond()).Find(&trackedTimes)
return
}
err = x.Where(options.ToCond()).Find(&trackedTimes)
err = options.ToSession(x).Find(&trackedTimes)
return
}
@ -85,7 +94,7 @@ func AddTime(user *User, issue *Issue, time int64) (*TrackedTime, error) {
Issue: issue,
Repo: issue.Repo,
Doer: user,
Content: secToTime(time),
Content: SecToTime(time),
Type: CommentTypeAddTimeManual,
}); err != nil {
return nil, err
@ -115,7 +124,7 @@ func TotalTimes(options FindTrackedTimesOptions) (map[*User]string, error) {
}
return nil, err
}
totalTimes[user] = secToTime(total)
totalTimes[user] = SecToTime(total)
}
return totalTimes, nil
}

View File

@ -97,14 +97,17 @@ func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) {
}
// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
func InitOAuth2() {
oauth2.Init()
func InitOAuth2() error {
if err := oauth2.Init(x); err != nil {
return err
}
loginSources, _ := GetActiveOAuth2ProviderLoginSources()
for _, source := range loginSources {
oAuth2Config := source.OAuth2()
oauth2.RegisterProvider(source.Name, oAuth2Config.Provider, oAuth2Config.ClientID, oAuth2Config.ClientSecret, oAuth2Config.OpenIDConnectAutoDiscoveryURL, oAuth2Config.CustomURLMapping)
}
return nil
}
// wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2

View File

@ -7,13 +7,12 @@ package oauth2
import (
"math"
"net/http"
"os"
"path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/gorilla/sessions"
"github.com/go-xorm/xorm"
"github.com/lafriks/xormstore"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/bitbucket"
@ -41,13 +40,14 @@ type CustomURLMapping struct {
}
// Init initialize the setup of the OAuth2 library
func Init() {
sessionDir := filepath.Join(setting.AppDataPath, "sessions", "oauth2")
if err := os.MkdirAll(sessionDir, 0700); err != nil {
log.Fatal(4, "Fail to create dir %s: %v", sessionDir, err)
}
func Init(x *xorm.Engine) error {
store, err := xormstore.NewOptions(x, xormstore.Options{
TableName: "oauth2_session",
}, []byte(sessionUsersStoreKey))
store := sessions.NewFilesystemStore(sessionDir, []byte(sessionUsersStoreKey))
if err != nil {
return err
}
// according to the Goth lib:
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
@ -65,6 +65,7 @@ func Init() {
return req.Header.Get(providerHeaderKey), nil
}
return nil
}
// Auth OAuth2 auth service

View File

@ -179,8 +179,9 @@ func NewFuncMap() []template.FuncMap {
}
return dict, nil
},
"Printf": fmt.Sprintf,
"Escape": Escape,
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": models.SecToTime,
}}
}

View File

@ -13,10 +13,11 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"net/http/httptest"
"github.com/go-macaron/session"
"github.com/stretchr/testify/assert"
"gopkg.in/macaron.v1"
"net/http/httptest"
)
// MockContext mock context for unit tests
@ -48,6 +49,16 @@ func LoadRepo(t *testing.T, ctx *context.Context, repoID int64) {
ctx.Repo.RepoLink = ctx.Repo.Repository.Link()
}
// LoadRepoCommit loads a repo's commit into a test context.
func LoadRepoCommit(t *testing.T, ctx *context.Context) {
gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath())
assert.NoError(t, err)
branch, err := gitRepo.GetHEADBranch()
assert.NoError(t, err)
ctx.Repo.Commit, err = gitRepo.GetBranchCommit(branch.Name)
assert.NoError(t, err)
}
// LoadUser load a user into a test context.
func LoadUser(t *testing.T, ctx *context.Context, userID int64) {
ctx.User = models.AssertExistsAndLoadBean(t, &models.User{ID: userID}).(*models.User)

View File

@ -28,7 +28,7 @@ password=Passwort
re_type=Passwort erneut eingeben
captcha=CAPTCHA
twofa=Zwei-Faktor-Authentifizierung
twofa_scratch=Zwei-Faktor-Scratch-Code
twofa_scratch=Zwei-Faktor-Einmalpasswort
passcode=PIN
repository=Repository
@ -81,10 +81,12 @@ err_empty_admin_password=Das Administrator-Passwort darf nicht leer sein.
general_title=Allgemeine Einstellungen
app_name=Seitentitel
app_name_helper=Gebe hier den Namen deines Unternehmens ein.
repo_path=Repository-Verzeichnis
repo_path_helper=Remote-Git-Repositories werden in diesem Verzeichnis gespeichert.
lfs_path=Git LFS-Wurzelpfad
lfs_path_helper=In diesem Verzeichnis werden die Dateien von Git LFS abgespeichert. Leer lassen um LFS zu deaktivieren.
run_user=Ausführen als
run_user_helper=Gebe den Betriebssystem-Benutzernamen ein, unter welchem Gitea laufen soll. Beachte, dass dieser Nutzer Zugriff auf den Repository-Ordner haben muss.
domain=SSH Server-Domain
domain_helper=Domain oder Host-Adresse für die SSH-URL.
@ -144,6 +146,7 @@ default_allow_create_organization=Erstellen von Organisationen standarmäßig er
default_allow_create_organization_popup=Neuen Nutzern das Erstellen von Organisationen standardmäßig erlauben.
default_enable_timetracking=Zeiterfassung standardmäßig aktivieren
default_enable_timetracking_popup=Zeiterfassung standardmäßig für neue Repositories aktivieren.
no_reply_address_helper=Domain-Namen für Benutzer mit einer versteckten Emailadresse. Zum Beispiel wird der Benutzername "Joe" in Git als "joe@noreply.example.org" protokolliert, wenn die versteckte E-Mail-Domäne "noreply.example.org" festgelegt ist.
[home]
uname_holder=E-Mail-Adresse oder Benutzername
@ -185,23 +188,29 @@ confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b>
reset_password_mail_sent_prompt=Eine E-Mail wurde an <b>%s</b> gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um das Passwort zurückzusetzen.
active_your_account=Aktiviere dein Konto
prohibit_login=Anmelden verboten
resent_limit_prompt=Du hast bereits eine Aktivierungs-E-Mail angefordert. Bitte warte 3 Minuten und probiere es dann nochmal.
has_unconfirmed_mail=Hallo %s, du hast eine unbestätigte E-Mail-Adresse (<b>%s</b>). Wenn du keine Bestätigungs-E-Mail erhalten hast oder eine neue senden möchtest, klicke bitte auf den folgenden Button.
resend_mail=Aktivierungs-E-Mail erneut verschicken
email_not_associate=Diese E-Mail-Adresse ist mit keinem Konto verknüpft.
send_reset_mail=E-Mail zum Passwort-zurücksetzen erneut verschicken
reset_password=Passwort zurücksetzen
invalid_code=Dein Bestätigungs-Code ist ungültig oder abgelaufen.
reset_password_helper=Passwort zurückzusetzen
password_too_short=Das Passwort muss mindenstens %d Zeichen lang sein.
non_local_account=Benutzer, die nicht von Gitea verwaltet werden können ihre Passwörter nicht über das Web Interface ändern.
verify=Verifizieren
scratch_code=Einmalpasswort
use_scratch_code=Einmalpasswort verwenden
twofa_scratch_used=Du hast dein Einmalpasswort verwendet. Du wurdest zu den Einstellung der Zwei-Faktor-Authentifizierung umgeleitet, dort kannst du dein Gerät abmelden oder ein neues Einmalpasswort erzeugen.
twofa_passcode_incorrect=Ungültige PIN. Wenn du dein Gerät verloren hast, verwende dein Einmalpasswort.
twofa_scratch_token_incorrect=Das Einmalpasswort ist falsch.
login_userpass=Anmelden
login_openid=OpenID
openid_connect_submit=Verbinden
openid_connect_title=Mit bestehendem Konto verbinden
openid_register_title=Neues Konto einrichten
openid_signin_desc=Gib deine OpenID-URI ein. Zum Beispiel: https://anne.me, bob.openid.org.cn oder gnusocial.net/carry.
disable_forgot_password_mail=Das Zurücksetzen von Passwörtern wurde deaktiviert. Bitte wende dich an den Administrator.
[mail]
activate_account=Bitte aktiviere dein Konto
@ -236,6 +245,9 @@ TreeName=Dateipfad
Content=Inhalt
require_error=` darf nicht leer sein.`
alpha_dash_error=` sollte nur Buchstaben, Zahlen, Bindestriche ('-') und Unterstriche ('_') enthalten`
alpha_dash_dot_error=` sollte nur Buchstaben, Zahlen, Bindestriche ('-'), Unterstriche ('_') und Punkte ('.') enthalten`
git_ref_name_error=` muss ein wohlgeformter Git-Referenzname sein.`
size_error=` muss die Größe %s haben.`
min_size_error=` muss mindestens %s Zeichen enthalten.`
max_size_error=` darf höchstens %s Zeichen enthalten.`
@ -250,8 +262,11 @@ username_been_taken=Der Benutzername ist bereits vergeben.
repo_name_been_taken=Der Repository-Name wird schon verwendet.
org_name_been_taken=Der Organisationsname ist bereits vergeben.
team_name_been_taken=Der Teamname ist bereits vergeben.
team_no_units_error=Das Team muss auf mindestens einen Bereich Zugriff haben.
email_been_used=Die E-Mail-Adresse wird bereits verwendet.
openid_been_used=Die OpenID-Adresse "%s" wird bereits verwendet.
username_password_incorrect=Benutzername oder Passwort ist falsch.
enterred_invalid_repo_name=Der eingegebenen Repository-Name ist falsch.
user_not_exist=Dieser Benutzer ist nicht vorhanden.
auth_failed=Authentifizierung fehlgeschlagen: %v
@ -290,11 +305,13 @@ organization=Organisationen
uid=Uid
public_profile=Öffentliches Profil
password_username_disabled=Benutzer, die nicht von Gitea verwaltet werden können ihren Benutzernamen nicht ändern. Bitte kontaktiere deinen Administrator für mehr Details.
full_name=Vollständiger Name
website=Webseite
location=Standort
update_profile=Profil aktualisieren
update_profile_success=Dein Profil wurde aktualisiert.
change_username=Dein Benutzername wurde geändert.
continue=Weiter
cancel=Abbrechen
@ -309,6 +326,7 @@ change_password=Passwort aktualisieren
old_password=Aktuelles Passwort
new_password=Neues Passwort
retype_new_password=Neues Passwort erneut eingeben
password_change_disabled=Benutzer, die nicht von Gitea verwaltet werden, können ihr Passwort im Web Interface nicht ändern.
emails=E-Mail-Adressen
email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen und andere Funktionen verwendet.
@ -372,6 +390,7 @@ delete_account_title=Benutzerkonto löschen
[repo]
owner=Besitzer
repo_name=Repository-Name
repo_name_helper=Ein guter Repository-Name besteht normalerweise aus kurzen, unvergesslichen und einzigartigen Schlagwörtern.
visibility=Sichtbarkeit
fork_repo=Repository forken
fork_from=Fork von

View File

@ -737,6 +737,7 @@ issues.add_time_sum_to_small = No time was entered.
issues.cancel_tracking = Cancel
issues.cancel_tracking_history = `cancelled time tracking %s`
issues.time_spent_total = Total time spent
issues.time_spent_from_all_authors = `Total Time Spent: %s`
issues.dependency.title = Dependencies
issues.dependency.issue_no_dependencies = This issue currently doesn't have any dependencies.
issues.dependency.pr_no_dependencies = This pull request currently doesn't have any dependencies.

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,15 @@
app_desc=Зручний сервіс, власного Git хостінгу
home=Головна
dashboard=Панель інструментів
explore=Огляд
help=Довідка
sign_in=Увійти
sign_in_with=Увійти через
sign_out=Вийти
sign_up=Реєстрація
link_account=Прив'язати обліковий запис
link_account_signin_or_signup=Увійдіть з уже існуючими обліковими даними або зареєструйтеся для зв'язку з цим аккаунтом.
register=Реєстрація
website=Web-сайт
version=Версія
@ -12,10 +17,17 @@ page=Сторінка
template=Шаблон
language=Мова
notifications=Сповіщення
create_new=Створити…
user_profile_and_more=Профіль і налаштування…
signed_in_as=Увійшов як
enable_javascript=Цей веб-сайт працює краще з JavaScript.
username=Ім'я кристувача
email=Адреса електронної пошти
password=Пароль
re_type=Введіть пароль ще раз
captcha=CAPTCHA
twofa=Двофакторна авторизація
passcode=Код доступу
repository=Репозиторій
@ -29,6 +41,9 @@ new_org=Нова організація
manage_org=Керування організаціями
account_settings=Параметри облікового запису
settings=Параметри
your_profile=Профіль
your_starred=Обрані
your_settings=Параметри
all=Усі
sources=Джерела
@ -36,38 +51,71 @@ mirrors=Дзеркала
collaborative=Співпраця
forks=Відгалуження
pull_requests=Запити до злиття
activities=Дії
pull_requests=Запити на злиття
issues=Питання
cancel=Відміна
[install]
install=Встановлення
title=Початкова конфігурація
docker_helper=Якщо ви запускаєте Gitea всередині Docker, будь ласка уважно прочитайте <a target="_blank" rel="noopener" href="%s">документацію</a> перед тим, як що-небудь змінити на цій сторінці.
requite_db_desc=Gitea потребує MySQL, PostgreSQL, MSSQL, SQLite3 або TiDB.
db_title=Налаштування бази даних
db_type=Тип бази даних
host=Хост
user=Ім'я кристувача
password=Пароль
db_name=Ім'я бази даних
ssl_mode=SSL
path=Шлях
err_empty_db_path=Шлях до бази даних SQLite3 або TiDB не може бути порожнім.
no_admin_and_disable_registration=Ви не можете вимкнути реєстрацію до створення облікового запису адміністратора.
general_title=Загальні налаштування
app_name=Назва сайту
repo_path=Кореневий шлях репозиторія
repo_path_helper=Всі вилучені Git репозиторії будуть збережені в цей каталог.
lfs_path=Кореневої шлях Git LFS
lfs_path_helper=У цій папці будуть зберігатися файли Git LFS. Залиште порожнім, щоб відключити LFS.
ssh_port=Порт SSH сервера
ssh_port_helper=Номер порту, який використовує SSH сервер. Залиште порожнім, щоб відключити SSH.
app_url=Базова URL-адреса Gitea
log_root_path=Шлях до лог файлу
optional_title=Додаткові налаштування
email_title=Налаштування Email
smtp_host=SMTP хост
smtp_from=Відправляти Email від імені
mailer_user=SMTP Ім'я кристувача
mailer_password=SMTP Пароль
mail_notify=Дозволити поштові повідомлення
disable_gravatar=Вимкнути Gravatar
federated_avatar_lookup=Увімкнути зовнішні аватари
federated_avatar_lookup_popup=Увімкнути зовнішний Аватар за допомогою Libravatar.
openid_signin=Увімкнути реєстрацію за допомогою OpenID
enable_captcha=Увімкнути CAPTCHA
enable_captcha_popup=Вимагати перевірку CAPTCHA при самостійній реєстрації користувача.
admin_name=Ім'я кристувача Адміністратора
admin_password=Пароль
confirm_password=Підтвердження пароля
admin_email=Адреса електронної пошти
install_btn_confirm=Встановлення Gitea
test_git_failed=Не в змозі перевірити 'git' команду: %v
save_config_failed=Не в змозі зберегти конфігурацію: %v
install_success=Ласкаво просимо! Дякуємо вам за вибір Gitea. Розважайтеся, і будьте обережні!
[home]
password_holder=Пароль
switch_dashboard_context=Змінити дошку
my_repos=Мої репозиторії
show_more_repos=Показати більше репозиторіїв…
collaborative_repos=Спільні репозиторії
my_orgs=Мої організації
my_mirrors=Мої дзеркала
view_home=Переглянути %s
search_repos=Шукати репозиторій…
issues.in_your_repos=В ваших репозиторіях
@ -76,21 +124,43 @@ repos=Репозиторії
users=Користувачі
organizations=Організації
search=Пошук
code=Код
[auth]
create_new_account=Реєстрація аккаунта
register_helper_msg=Вже зареєстровані? Увійдіть зараз!
disable_register_prompt=Вибачте, можливість реєстрації відключена. Будь ласка, зв'яжіться з адміністратором сайту.
remember_me=Запам'ятати мене
forgot_password_title=Забув пароль
forgot_password=Забули пароль?
sign_up_now=Потрібен аккаунт? Зареєструватися.
confirmation_mail_sent_prompt=Новий лист для підтвердження було направлено на <b>%s</b>, будь ласка, перевірте вашу поштову скриньку протягом% s для завершення реєстрації.
reset_password_mail_sent_prompt=Лист для підтвердження було направлено на <b>%s</b>. Будь ласка, перевірте вашу поштову скриньку протягом% s для скидання пароля.
active_your_account=Активувати обліковий запис
prohibit_login=Вхід заборонений
prohibit_login_desc=Вхід для вашого профілю був заборонений, будь ласка, зв'яжіться з адміністратором сайту.
resent_limit_prompt=Вибачте, ви вже запросили активацію по електронній пошті нещодавно. Будь ласка, зачекайте 3 хвилини, а потім спробуйте ще раз.
has_unconfirmed_mail=Привіт %s, у вас є непідтвердженими адреси (<b>%s</b>). Якщо ви не отримали підтвердження електронною поштою або треба відправити нове, будь ласка, натисніть на кнопку нижче.
resend_mail=Натисніть тут, щоб вислати лист активації знову
email_not_associate=Ця електронна пошта не пов'язана ні з одним обліковим записом.
send_reset_mail=Натисніть сюди, щоб відправити лист для скидання пароля
reset_password=Скинути пароль
invalid_code=Цей код підтвердження недійсний або закінчився.
reset_password_helper=Натисніть тут для скидання пароля
password_too_short=Довжина пароля не може бути меншою за %d.
non_local_account=Нелокальні акаунти не можуть змінити пароль через Gitea.
verify=Підтвердити
scratch_code=Одноразовий пароль
use_scratch_code=Використовувати одноразовий пароль
twofa_scratch_used=Ви використовували scratch-код. Ви були перенаправлені на сторінку налаштувань для генерації нового коду або відключення двуфакторной аутентифікації.
twofa_passcode_incorrect=Ваш пароль є невірним. Якщо ви втратили пристрій, використовуйте ваш одноразовий пароль.
twofa_scratch_token_incorrect=Невірний одноразовий пароль.
login_userpass=Увійти
login_openid=OpenID
openid_connect_submit=Під’єднатися
openid_connect_title=Підключитися до існуючого облікового запису
openid_register_title=Створити новий обліковий запис
disable_forgot_password_mail=На жаль скидання пароля відключене. Будь ласка, зв'яжіться з адміністратором сайту.
[mail]
activate_account=Будь ласка, активуйте ваш обліковий запис
@ -102,14 +172,17 @@ register_notify=Ласкаво просимо у Gitea
[modal]
yes=Так
no=Ні
modify=Оновлення
[form]
UserName=Ім’я користувача
RepoName=Назва репозиторію
Email=Адреса електронної пошти
Password=Пароль
Retype=Введіть пароль ще раз
SSHTitle=Iм'я SSH ключа
HttpsUrl=Адреса HTTPS
PayloadUrl=URL обробника
TeamName=Назва команди
AuthName=Назва авторизації
AdminEmail=Email адміністратора
@ -121,10 +194,18 @@ TreeName=Шлях до файлу
Content=Зміст
require_error=` не може бути пустим.`
git_ref_name_error=` має бути правильним посилальним ім'ям Git.`
email_error=` не є адресою електронної пошти.`
unknown_error=Невідома помилка:
password_not_match=Паролі не співпадають.
username_been_taken=Ім'я користувача вже зайнято.
repo_name_been_taken=Ім'я репозіторію вже використовується.
email_been_used=Ця електронна адреса вже використовується.
username_password_incorrect=Неправильне ім'я користувача або пароль.
user_not_exist=Даний користувач не існує.
auth_failed=Помилка автентифікації: %v
@ -133,6 +214,7 @@ join_on=Приєднався
repositories=Репозиторії
activity=Публічна активність
followers=Підписники
starred=Обрані Репозиторії
following=Слідкувати
follow=Підписатися
unfollow=Відписатися
@ -144,10 +226,14 @@ profile=Профіль
password=Пароль
security=Безпека
avatar=Аватар
ssh_gpg_keys=SSH / GPG ключи
social=Соціальні акаунти
applications=Токени Доступу
orgs=Керування організаціями
repos=Репозиторії
delete=Видалити обліковий запис
twofa=Двофакторна авторизація
organization=Організації
uid=Ідентифікатор Uid
public_profile=Загальнодоступний профіль
@ -159,16 +245,35 @@ update_profile_success=Профіль успішно оновлено.
continue=Продовжити
cancel=Відміна
federated_avatar_lookup=Знайти зовнішній аватар
enable_custom_avatar=Увімкнути користувацькі аватари
choose_new_avatar=Оберіть новий аватар
delete_current_avatar=Видалити поточний аватар
change_password=Оновити пароль
old_password=Поточний пароль
new_password=Новий пароль
emails=Адреса електронної пошти
email_desc=Ваша основна адреса електронної пошти використовуватиметься для сповіщення та інших операцій.
primary=Основний
delete_email=Видалити
manage_ssh_keys=Керувати SSH ключами
manage_gpg_keys=Керувати GPG ключами
add_key=Додати ключ
ssh_helper=<strong>Потрібна допомога?</strong> Дивіться гід на GitHub з <a href="%s"> генерації ключів SSH</a> або виправлення <a href="%s">типових неполадок SSH</a>.
add_new_key=Додати SSH ключ
add_new_gpg_key=Додати GPG ключ
key_id=ID ключа
key_name=Ім'я ключа
key_content=Зміст
delete_key=Видалити
ssh_key_deletion=Видалити SSH ключ
gpg_key_deletion=Видалити GPG ключ
add_on=Додано
valid_until=Дійсний до
valid_forever=Дійсний завжди
last_used=Останнє використання
no_activity=Жодної діяльності
can_read_info=Читати
@ -178,7 +283,11 @@ token_state_desc=Цей токен використовувався в оста
show_openid=Показати у профілю
hide_openid=Не показувати у профілі
manage_social=Керувати зв'язаними аккаунтами соціальних мереж
generate_new_token=Згенерувати новий токен
token_name=Ім'я токену
generate_token=Згенерувати токен
delete_token=Видалити
@ -186,37 +295,49 @@ delete_token=Видалити
delete_account=Видалити ваш обліковий запис
confirm_delete_account=Підтвердження видалення
delete_account_title=Видалити цей обліковий запис
[repo]
owner=Власник
repo_name=Назва репозиторію
visibility=Видимість
visiblity_helper=Зробити репозиторій приватним
fork_repo=Відгалужити репозиторій
fork_from=Відгалужена з
repo_desc=Опис
repo_lang=Мова
repo_gitignore_helper=Виберіть шаблон .gitignore.
license=Ліцензія
license_helper=Виберіть ліцензійний файл.
readme_helper=Виберіть шаблон README.
create_repo=Створити репозиторій
default_branch=Головна гілка
mirror_prune=Очистити
watchers=Спостерігачі
form.reach_limit_of_creation=Ви досягли максимальної кількості %d створених репозиторіїв.
migrate_type=Тип міграції
migrate_type_helper=Даний репозиторій буде <span class="text blue">дзеркалом</span>
migrate_repo=Перенесення репозиторія
migrate.failed=Міграція не вдалася: %v
forked_from=відгалужено від
copy_link=Копіювати
copied=Скопійовано
unwatch=Не стежити
watch=Слідкувати
unstar=Зняти зірку
star=Зірка
unstar=Видалити із обраних
star=В обрані
fork=Відгалуження
download_archive=Скачати репозиторій
no_desc=Без опису
quick_guide=Короткий посібник
clone_this_repo=Кнонувати цей репозиторій
create_new_repo_command=Створити новий репозиторій з командного рядка
push_exist_repo=Опублікувати існуючий репозиторій з командного рядка
bare_message=Цей репозиторій порожній.
code=Код
branch=Гілка
@ -225,7 +346,7 @@ filter_branch_and_tag=Фільтрувати гілку або тег
branches=Гілки
tags=Теги
issues=Питання
pulls=Запити до злиття
pulls=Запити на злиття
labels=Мітки
milestones=Етап
commits=Зміни
@ -236,13 +357,26 @@ file_history=Історія
file_view_raw=Перегляд Raw
file_permalink=Постійне посилання
editor.new_file=Новий файл
editor.upload_file=Завантажити файл
editor.edit_file=Редагування файла
editor.preview_changes=Попередній перегляд змін
editor.edit_this_file=Редагування файла
editor.delete_this_file=Видалити файл
editor.name_your_file=Дайте назву файлу…
editor.or=або
editor.cancel_lower=Скасувати
editor.commit_changes=Зафіксувати зміни
editor.add_tmpl=Додати '%s/<filename>'
editor.add=Додати '%s'
editor.update=Оновити '%s'
editor.delete=Видалити '%s'
editor.cancel=Відміна
editor.upload_files_to_dir=Завантажувати файли до '%s'
commits.commits=Зміни
commits.find=Пошук
commits.search_all=Усі гілки
commits.author=Автор
commits.message=Повідомлення
commits.date=Дата
@ -263,17 +397,54 @@ issues.new.assignee=Призначено
issues.new.clear_assignee=Прибрати відповідального
issues.new.no_assignee=Немає відповідального
issues.new_label=Нова мітка
issues.new_label_placeholder=Назва мітки
issues.new_label_desc_placeholder=Опис
issues.create_label=Створити мітку
issues.label_templates.helper=Оберіть набір міток
issues.label_templates.fail_to_load_file=Не вдалося завантажити файл шаблона мітки '%s': %v
issues.deleted_milestone=`(видалено)`
issues.open_tab=%d відкрито
issues.close_tab=%d закрито
issues.filter_label=Мітка
issues.filter_milestone=Етап
issues.filter_milestone_no_select=Всі етапи
issues.filter_assignee=Відповідальний
issues.filter_type=Тип
issues.filter_type.all_issues=Всі проблемы
issues.filter_type.created_by_you=Створено вами
issues.filter_type.mentioning_you=Вас згадано
issues.filter_sort=Сортувати
issues.filter_sort.latest=Найновіші
issues.filter_sort.oldest=Найстаріші
issues.filter_sort.recentupdate=Нещодавно оновлено
issues.filter_sort.leastupdate=Найдавніше оновлені
issues.filter_sort.mostcomment=Найбільш коментовані
issues.filter_sort.leastcomment=Найменш коментовані
issues.action_open=Відкрити
issues.action_close=Закрити
issues.action_label=Мітка
issues.action_milestone=Етап
issues.action_milestone_no_select=Етап відсутній
issues.action_assignee=Відповідальний
issues.action_assignee_no_select=Немає відповідального
issues.opened_by=%[1]s відкрито <a href="%[2]s">%[3]s</a>
issues.opened_by_fake=%[1]s відкрито %[2]s
issues.previous=Попередній
issues.next=Далі
issues.open_title=Відкрити
issues.closed_title=Закриті
issues.num_comments=%d коментарів
issues.commented_at=`відкоментовано <a href="#%s">%s</a>`
issues.delete_comment_confirm=Ви впевнені, що хочете видалити цей коментар?
issues.close_issue=Закрити
issues.close_comment_issue=Прокоментувати і закрити
issues.reopen_issue=Відкрити знову
issues.create_comment=Коментар
issues.closed_at=`закрито <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at=`згадано цю проблему в коміті <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.collaborator=Співавтор
issues.owner=Власник
issues.sign_in_require_desc=<a href="%s">Підпишіться</a> щоб приєднатися до обговорення.
issues.edit=Редагувати
issues.cancel=Відміна
issues.save=Зберегти
@ -287,18 +458,35 @@ issues.label.filter_sort.alphabetically=За абеткою
issues.label.filter_sort.reverse_alphabetically=Зворотною абеткою
issues.label.filter_sort.by_size=Розмір
issues.label.filter_sort.reverse_by_size=Зворотний розмір
issues.num_participants=%d учасників
issues.attachment.open_tab=`Натисніть щоб побачити "%s" у новій вкладці`
issues.subscribe=Підписатися
issues.unsubscribe=Відписатися
issues.start_tracking_short=Запустити
issues.stop_tracking=Стоп
issues.add_time_short=Додати час
issues.add_time_cancel=Відміна
issues.add_time_hours=Години
issues.add_time_minutes=Хвилини
issues.cancel_tracking=Відміна
pulls.new=Новий запит на злиття
pulls.compare_changes=Новий запит на злиття
pulls.filter_branch=Фільтр по гілці
pulls.no_results=Результатів не знайдено.
pulls.create=Створити запит на злиття
pulls.title_desc=хоче злити %[1]d комітів з <code>%[2]s</code> до <code>%[3]s</code>
pulls.tab_commits=Комітів
pulls.reopen_to_merge=Будь ласка перевідкрийте цей запит щоб здіснити операцію злиття.
pulls.merged=Злито
pulls.can_auto_merge_desc=Цей запит можна об'єднати автоматично.
pulls.merge_pull_request=Об'єднати запит на злиття
milestones.new=Новий етап
milestones.open_tab=%d відкрито
milestones.close_tab=%d закрито
milestones.closed=Закрито %s
milestones.no_due_date=Немає дати завершення
milestones.open=Відкрити
milestones.close=Закрити
milestones.create=Створити етап
@ -308,14 +496,31 @@ milestones.due_date=Дата завершення (опціонально)
milestones.clear=Очистити
milestones.edit=Редагувати етап
milestones.cancel=Відміна
milestones.modify=Оновити етап
wiki=Wiki
wiki.page=Сторінка
wiki.filter_page=Фільтр сторінок
wiki.new_page=Сторінка
wiki.save_page=Зберегти сторінку
wiki.edit_page_button=Редагувати
wiki.new_page_button=Нова сторінка
wiki.delete_page_button=Видалити сторінку
wiki.pages=Сторінки
wiki.last_updated=Останні оновлення %s
activity=Активність
activity.period.filter_label=Період:
activity.period.daily=1 день
activity.period.halfweekly=3 дні
activity.period.weekly=1 тиждень
activity.period.monthly=1 місяць
activity.overview=Огляд
activity.active_prs_count_n=<strong>%d</strong> Активні запити на злиття
activity.merged_prs_count_1=Об'єднати запит на злиття
activity.merged_prs_count_n=Об'єднати запити на злиття
activity.merged_prs_label=Злито
activity.closed_issue_label=Закриті
activity.new_issues_count_1=Нове обговорення
activity.new_issues_count_n=Нове обговорення
@ -327,42 +532,114 @@ search=Пошук
search.search_repo=Пошук репозиторію
settings=Параметри
settings.options=Репозиторій
settings.collaboration.admin=Адміністратор
settings.collaboration.write=Написати
settings.collaboration.read=Читати
settings.collaboration.undefined=Не визначено
settings.hooks=Webhooks
settings.basic_settings=Базові налаштування
settings.mirror_settings=Налаштування дзеркала
settings.update_settings=Оновити налаштування
settings.advanced_settings=Додаткові налаштування
settings.tracker_issue_style.numeric=Цифровий
settings.tracker_issue_style.alphanumeric=Буквено-цифровий
settings.danger_zone=Небезпечна зона
settings.transfer_owner=Новий власник
settings.add_webhook=Додати Webhook
settings.webhook_deletion=Видалити Webhook
settings.webhook.request=Запит
settings.secret=Секрет
settings.slack_username=Ім'я кристувача
settings.slack_icon_url=URL іконки
settings.discord_username=Ім'я кристувача
settings.discord_icon_url=URL іконки
settings.slack_color=Колір
settings.event_create=Створити
settings.event_pull_request=Запити до злиття
settings.event_push=Push
settings.event_repository=Репозиторій
settings.update_webhook=Оновити Webhook
settings.slack_token=Токен
settings.slack_domain=Домен
settings.slack_channel=Канал
settings.deploy_keys=Ключи для розгортування
settings.add_deploy_key=Додати ключ для розгортування
settings.is_writable=Включити доступ для запису
settings.title=Заголовок
settings.branches=Гілки
settings.protected_branch_can_push=Дозволити push?
settings.protected_branch_can_push_yes=Ви можете виконувати push
settings.protected_branch_can_push_no=Ви не можете виконувати push
settings.add_protected_branch=Увімкнути захист
settings.delete_protected_branch=Вимкнути захист
settings.choose_branch=Оберіть гілку…
diff.browse_source=Переглянути джерело
diff.commit=коміт
diff.show_split_view=Розділений перегляд
diff.view_file=Переглянути файл
diff.too_many_files=Деякі файли не було показано, через те що забагато файлів було змінено
release.releases=Релізи
release.new_release=Новий реліз
release.draft=Чернетка
release.prerelease=Пре-реліз
release.stable=Стабільний
release.edit=редагувати
release.ahead=<strong>%d</strong> комітів %s після цього релізу
release.tag_name=Назва тегу
release.target=Ціль
release.title=Заголовок
release.preview=Переглянути
release.loading=Завантаження…
release.cancel=Відміна
release.edit_release=Оновити реліз
release.delete_release=Видалити реліз
release.deletion=Видалити реліз
branch.delete_head=Видалити
topic.done=Готово
[org]
org_name_holder=Назва організації
org_full_name_holder=Повна назва організації
settings=Налаштування
settings.update_settings=Оновити налаштування
teams.settings=Налаштування
teams.update_settings=Оновити налаштування
teams.add_team_repository=Додати репозиторій команди
[admin]
dashboard.delete_inactivate_accounts_success=Усі неактивні облікові записи успішно видалено.
dashboard.current_memory_usage=Поточне використання пам'яті
dashboard.memory_obtained=Отримано пам'яті
users.name=Ім'я кристувача
config.db_config=Налаштування бази даних
config.db_type=Тип
config.db_host=Хост
config.db_name=Ім'я
config.db_user=Ім'я кристувача
config.db_ssl_mode=SSL
config.db_ssl_mode_helper=(тільки для "postgres")
config.db_path=Шлях
config.db_path_helper=(для "sqlite3" і "tidb")
config.webhook_config=Налаштування Webhook
@ -374,12 +651,21 @@ teams.add_team_repository=Додати репозиторій команди
[action]
merge_pull_request=`запит на злиття злито <a href="%s/pulls/%s">%s#%[2]s</a>`
[tool]
[dropzone]
file_too_big=Розмір файлу ({{filesize}} MB), що більше ніж максимальний розмір: ({{maxFilesize}} MB).
remove_file=Видалити файл
[notification]
notifications=Сповіщення
unread=Непрочитані
read=Прочитані
mark_as_read=Позначити як прочитане
mark_as_unread=Позначити як непрочитане
mark_all_as_read=Позначити всі як прочитані
[gpg]

View File

@ -1565,6 +1565,46 @@
}
}
},
"/repos/{owner}/{repo}/hooks/{id}/tests": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Test a push webhook",
"operationId": "repoTestHook",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "id of the hook to test",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
}
}
}
},
"/repos/{owner}/{repo}/issue/{index}/comments": {
"get": {
"produces": [

View File

@ -382,9 +382,12 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/hooks", func() {
m.Combo("").Get(repo.ListHooks).
Post(bind(api.CreateHookOption{}), repo.CreateHook)
m.Combo("/:id").Get(repo.GetHook).
Patch(bind(api.EditHookOption{}), repo.EditHook).
Delete(repo.DeleteHook)
m.Group("/:id", func() {
m.Combo("").Get(repo.GetHook).
Patch(bind(api.EditHookOption{}), repo.EditHook).
Delete(repo.DeleteHook)
m.Post("/tests", context.RepoRef(), repo.TestHook)
})
}, reqToken(), reqRepoWriter())
m.Group("/collaborators", func() {
m.Get("", repo.ListCollaborators)

View File

@ -123,13 +123,10 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) {
// responses:
// "200":
// "$ref": "#/responses/Team"
team := &models.Team{
ID: ctx.Org.Team.ID,
OrgID: ctx.Org.Team.OrgID,
Name: form.Name,
Description: form.Description,
Authorize: models.ParseAccessMode(form.Permission),
}
team := ctx.Org.Team
team.Name = form.Name
team.Description = form.Description
team.Authorize = models.ParseAccessMode(form.Permission)
if err := models.UpdateTeam(team, true); err != nil {
ctx.Error(500, "EditTeam", err)
return

View File

@ -5,11 +5,11 @@
package repo
import (
"code.gitea.io/git"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/convert"
"code.gitea.io/gitea/routers/api/v1/utils"
api "code.gitea.io/sdk/gitea"
)
@ -82,6 +82,62 @@ func GetHook(ctx *context.APIContext) {
ctx.JSON(200, convert.ToHook(repo.RepoLink, hook))
}
// TestHook tests a hook
func TestHook(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/hooks/{id}/tests repository repoTestHook
// ---
// summary: Test a push webhook
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the hook to test
// type: integer
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
if ctx.Repo.Commit == nil {
// if repo does not have any commits, then don't send a webhook
ctx.Status(204)
return
}
hookID := ctx.ParamsInt64(":id")
hook, err := utils.GetRepoHook(ctx, ctx.Repo.Repository.ID, hookID)
if err != nil {
return
}
if err := models.PrepareWebhook(hook, ctx.Repo.Repository, models.HookEventPush, &api.PushPayload{
Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch,
Before: ctx.Repo.Commit.ID.String(),
After: ctx.Repo.Commit.ID.String(),
Commits: []*api.PayloadCommit{
convert.ToCommit(ctx.Repo.Repository, ctx.Repo.Commit),
},
Repo: ctx.Repo.Repository.APIFormat(models.AccessModeNone),
Pusher: ctx.User.APIFormat(),
Sender: ctx.User.APIFormat(),
}); err != nil {
ctx.Error(500, "PrepareWebhook: ", err)
return
}
go models.HookQueue.Add(ctx.Repo.Repository.ID)
ctx.Status(204)
}
// CreateHook create a hook for a repository
func CreateHook(ctx *context.APIContext, form api.CreateHookOption) {
// swagger:operation POST /repos/{owner}/{repo}/hooks repository repoCreateHook

View File

@ -0,0 +1,33 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"net/http"
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestTestHook(t *testing.T) {
models.PrepareTestEnv(t)
ctx := test.MockContext(t, "user2/repo1/wiki/_pages")
ctx.SetParams(":id", "1")
test.LoadRepo(t, ctx, 1)
test.LoadRepoCommit(t, ctx)
test.LoadUser(t, ctx, 2)
TestHook(&context.APIContext{Context: ctx, Org: nil})
assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status())
models.AssertExistsAndLoadBean(t, &models.HookTask{
RepoID: 1,
HookID: 1,
}, models.Cond("is_delivered=?", false))
}

View File

@ -0,0 +1,16 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/models"
)
func TestMain(m *testing.M) {
models.MainTest(m, filepath.Join("..", "..", "..", ".."))
}

View File

@ -60,7 +60,9 @@ func GlobalInit() {
log.Fatal(4, "Failed to initialize ORM engine: %v", err)
}
models.HasEngine = true
models.InitOAuth2()
if err := models.InitOAuth2(); err != nil {
log.Fatal(4, "Failed to initialize OAuth2 support: %v", err)
}
models.LoadRepoConfig()
models.NewRepoContext()

View File

@ -1171,6 +1171,12 @@ func Milestones(ctx *context.Context) {
ctx.ServerError("GetMilestones", err)
return
}
if ctx.Repo.Repository.IsTimetrackerEnabled() {
if miles.LoadTotalTrackedTimes(); err != nil {
ctx.ServerError("LoadTotalTrackedTimes", err)
return
}
}
for _, m := range miles {
m.RenderedContent = string(markdown.Render([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()))
}

View File

@ -198,6 +198,10 @@
<span class="comment ui right"><i class="octicon octicon-comment"></i> {{.NumComments}}</span>
{{end}}
{{if .TotalTrackedTime}}
<span class="comment ui right"><i class="octicon octicon-clock"></i> {{.TotalTrackedTime | Sec2Time}}</span>
{{end}}
<p class="desc">
{{$.i18n.Tr "repo.issues.opened_by" $timeStr .Poster.HomeLink .Poster.Name | Safe}}
{{$tasks := .GetTasks}}

View File

@ -64,6 +64,7 @@
<span class="issue-stats">
<i class="octicon octicon-issue-opened"></i> {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}}
<i class="octicon octicon-issue-closed"></i> {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}}
{{if .TotalTrackedTime}}<i class="octicon octicon-clock"></i> {{.TotalTrackedTime|Sec2Time}}{{end}}
</span>
</div>
{{if $.IsRepositoryWriter}}

View File

@ -172,7 +172,7 @@
{{if gt (len .WorkingUsers) 0}}
<div class="ui divider"></div>
<div class="ui participants comments">
<span class="text"><strong>{{.i18n.Tr "repo.issues.time_spent_total"}}</strong></span>
<span class="text"><strong>{{.i18n.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time) | Safe}}</strong></span>
<div>
{{range $user, $trackedtime := .WorkingUsers}}
<div class="comment">

View File

@ -79,6 +79,9 @@
{{if .NumComments}}
<span class="comment ui right"><i class="octicon octicon-comment"></i> {{.NumComments}}</span>
{{end}}
{{if .TotalTrackedTime}}
<span class="comment ui right"><i class="octicon octicon-clock"></i> {{.TotalTrackedTime | Sec2Time}}</span>
{{end}}
<p class="desc">
{{$.i18n.Tr "repo.issues.opened_by" $timeStr .Poster.HomeLink .Poster.Name | Safe}}

75
vendor/github.com/lafriks/xormstore/Gopkg.lock generated vendored Normal file
View File

@ -0,0 +1,75 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/denisenkom/go-mssqldb"
packages = ["."]
revision = "ee492709d4324cdcb051d2ac266b77ddc380f5c5"
[[projects]]
name = "github.com/go-sql-driver/mysql"
packages = ["."]
revision = "a0583e0143b1624142adab07e0e97fe106d99561"
version = "v1.3"
[[projects]]
branch = "master"
name = "github.com/go-xorm/builder"
packages = ["."]
revision = "488224409dd8aa2ce7a5baf8d10d55764a913738"
[[projects]]
name = "github.com/go-xorm/core"
packages = ["."]
revision = "da1adaf7a28ca792961721a34e6e04945200c890"
version = "v0.5.7"
[[projects]]
name = "github.com/go-xorm/xorm"
packages = ["."]
revision = "1933dd69e294c0a26c0266637067f24dbb25770c"
version = "v0.6.4"
[[projects]]
name = "github.com/gorilla/context"
packages = ["."]
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
version = "v1.1"
[[projects]]
name = "github.com/gorilla/securecookie"
packages = ["."]
revision = "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983"
version = "v1.1.1"
[[projects]]
name = "github.com/gorilla/sessions"
packages = ["."]
revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896"
version = "v1.1"
[[projects]]
branch = "master"
name = "github.com/lib/pq"
packages = [".","oid"]
revision = "88edab0803230a3898347e77b474f8c1820a1f20"
[[projects]]
name = "github.com/mattn/go-sqlite3"
packages = ["."]
revision = "6c771bb9887719704b210e87e934f08be014bdb1"
version = "v1.6.0"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["md4"]
revision = "c7dcf104e3a7a1417abc0230cb0d5240d764159d"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "bba98a94e8c6668ae9556b4978bbffdfc5d4d535d522c8865465335bfaa2fc70"
solver-name = "gps-cdcl"
solver-version = 1

50
vendor/github.com/lafriks/xormstore/Gopkg.toml generated vendored Normal file
View File

@ -0,0 +1,50 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/go-sql-driver/mysql"
version = "1.3.0"
[[constraint]]
name = "github.com/go-xorm/xorm"
version = "0.6.4"
[[constraint]]
name = "github.com/gorilla/context"
version = "1.1.0"
[[constraint]]
name = "github.com/gorilla/securecookie"
version = "1.1.1"
[[constraint]]
name = "github.com/gorilla/sessions"
version = "1.1.0"
[[constraint]]
branch = "master"
name = "github.com/lib/pq"
[[constraint]]
name = "github.com/mattn/go-sqlite3"
version = "1.6.0"

19
vendor/github.com/lafriks/xormstore/LICENSE generated vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2018 Lauris Bukšis-Haberkorns, Mattias Wadman
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

48
vendor/github.com/lafriks/xormstore/README.md generated vendored Normal file
View File

@ -0,0 +1,48 @@
[![GoDoc](https://godoc.org/github.com/lafriks/xormstore?status.svg)](https://godoc.org/github.com/lafriks/xormstore)
[![Build Status](https://travis-ci.org/lafriks/xormstore.svg?branch=master)](https://travis-ci.org/lafriks/xormstore)
[![codecov](https://codecov.io/gh/lafriks/xormstore/branch/master/graph/badge.svg)](https://codecov.io/gh/lafriks/xormstore)
#### XORM backend for gorilla sessions
go get github.com/lafriks/xormstore
#### Example
```go
// initialize and setup cleanup
store := xormstore.New(engine, []byte("secret"))
// db cleanup every hour
// close quit channel to stop cleanup
quit := make(chan struct{})
go store.PeriodicCleanup(1*time.Hour, quit)
```
```go
// in HTTP handler
func handlerFunc(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session")
session.Values["user_id"] = 123
store.Save(r, w, session)
http.Error(w, "", http.StatusOK)
}
```
For more details see [xormstore godoc documentation](https://godoc.org/github.com/lafriks/xormstore).
#### Testing
Just sqlite3 tests:
go test
All databases using docker:
./test
If docker is not local (docker-machine etc):
DOCKER_IP=$(docker-machine ip dev) ./test
#### License
xormstore is licensed under the MIT license. See [LICENSE](LICENSE) for the full license text.

70
vendor/github.com/lafriks/xormstore/test generated vendored Executable file
View File

@ -0,0 +1,70 @@
#!/bin/bash
DOCKER_IP=${DOCKER_IP:-127.0.0.1}
sqlite3() {
DATABASE_URI="sqlite3://file:dummy?mode=memory&cache=shared" go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic
return $?
}
postgres10() {
ID=$(docker run -p 5432 -d postgres:10-alpine)
PORT=$(docker port "$ID" 5432 | cut -d : -f 2)
DATABASE_URI="postgres://user=postgres password=postgres dbname=postgres host=$DOCKER_IP port=$PORT sslmode=disable" go test -v -race -cover
S=$?
docker rm -vf "$ID" > /dev/null
return $S
}
postgres96() {
ID=$(docker run -p 5432 -d postgres:9.6-alpine)
PORT=$(docker port "$ID" 5432 | cut -d : -f 2)
DATABASE_URI="postgres://user=postgres password=postgres dbname=postgres host=$DOCKER_IP port=$PORT sslmode=disable" go test -v -race -cover
S=$?
docker rm -vf "$ID" > /dev/null
return $S
}
postgres94() {
ID=$(docker run -p 5432 -d postgres:9.4-alpine)
PORT=$(docker port "$ID" 5432 | cut -d : -f 2)
DATABASE_URI="postgres://user=postgres password=postgres dbname=postgres host=$DOCKER_IP port=$PORT sslmode=disable" go test -v -race -cover
S=$?
docker rm -vf "$ID" > /dev/null
return $S
}
mysql57() {
ID=$(docker run \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_USER=mysql \
-e MYSQL_PASSWORD=mysql \
-e MYSQL_DATABASE=mysql \
-p 3306 -d mysql:5.7)
PORT=$(docker port "$ID" 3306 | cut -d : -f 2)
DATABASE_URI="mysql://mysql:mysql@tcp($DOCKER_IP:$PORT)/mysql?charset=utf8&parseTime=True" go test -v -race -cover
S=$?
docker rm -vf "$ID" > /dev/null
return $S
}
mariadb10() {
ID=$(docker run \
-e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_USER=mysql \
-e MYSQL_PASSWORD=mysql \
-e MYSQL_DATABASE=mysql \
-p 3306 -d mariadb:10)
PORT=$(docker port "$ID" 3306 | cut -d : -f 2)
DATABASE_URI="mysql://mysql:mysql@tcp($DOCKER_IP:$PORT)/mysql?charset=utf8&parseTime=True" go test -v -race -cover
S=$?
docker rm -vf "$ID" > /dev/null
return $S
}
sqlite3 || exit 1
postgres94 || exit 1
postgres96 || exit 1
postgres10 || exit 1
mysql57 || exit 1
mariadb10 || exit 1

60
vendor/github.com/lafriks/xormstore/util/time_stamp.go generated vendored Normal file
View File

@ -0,0 +1,60 @@
package util
import (
"time"
)
// TimeStamp defines a timestamp
type TimeStamp int64
// TimeStampNow returns now int64
func TimeStampNow() TimeStamp {
return TimeStamp(time.Now().Unix())
}
// Add adds seconds and return sum
func (ts TimeStamp) Add(seconds int64) TimeStamp {
return ts + TimeStamp(seconds)
}
// AddDuration adds time.Duration and return sum
func (ts TimeStamp) AddDuration(interval time.Duration) TimeStamp {
return ts + TimeStamp(interval/time.Second)
}
// Year returns the time's year
func (ts TimeStamp) Year() int {
return ts.AsTime().Year()
}
// AsTime convert timestamp as time.Time in Local locale
func (ts TimeStamp) AsTime() (tm time.Time) {
tm = time.Unix(int64(ts), 0).Local()
return
}
// AsTimePtr convert timestamp as *time.Time in Local locale
func (ts TimeStamp) AsTimePtr() *time.Time {
tm := time.Unix(int64(ts), 0).Local()
return &tm
}
// Format formats timestamp as
func (ts TimeStamp) Format(f string) string {
return ts.AsTime().Format(f)
}
// FormatLong formats as RFC1123Z
func (ts TimeStamp) FormatLong() string {
return ts.Format(time.RFC1123Z)
}
// FormatShort formats as short
func (ts TimeStamp) FormatShort() string {
return ts.Format("Jan 02, 2006")
}
// IsZero is zero time
func (ts TimeStamp) IsZero() bool {
return ts.AsTime().IsZero()
}

251
vendor/github.com/lafriks/xormstore/xormstore.go generated vendored Normal file
View File

@ -0,0 +1,251 @@
/*
Package xormstore is a XORM backend for gorilla sessions
Simplest form:
store, err := xormstore.New(engine, []byte("secret-hash-key"))
All options:
store, err := xormstore.NewOptions(
engine, // *xorm.Engine
xormstore.Options{
TableName: "sessions", // "sessions" is default
SkipCreateTable: false, // false is default
},
[]byte("secret-hash-key"), // 32 or 64 bytes recommended, required
[]byte("secret-encyption-key")) // nil, 16, 24 or 32 bytes, optional
if err != nil {
// xormstore can not be initialized
}
// some more settings, see sessions.Options
store.SessionOpts.Secure = true
store.SessionOpts.HttpOnly = true
store.SessionOpts.MaxAge = 60 * 60 * 24 * 60
If you want periodic cleanup of expired sessions:
quit := make(chan struct{})
go store.PeriodicCleanup(1*time.Hour, quit)
For more information about the keys see https://github.com/gorilla/securecookie
For API to use in HTTP handlers see https://github.com/gorilla/sessions
*/
package xormstore
import (
"encoding/base32"
"net/http"
"strings"
"time"
"github.com/lafriks/xormstore/util"
"github.com/go-xorm/xorm"
"github.com/gorilla/context"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
const sessionIDLen = 32
const defaultTableName = "sessions"
const defaultMaxAge = 60 * 60 * 24 * 30 // 30 days
const defaultPath = "/"
// Options for xormstore
type Options struct {
TableName string
SkipCreateTable bool
}
// Store represent a xormstore
type Store struct {
e *xorm.Engine
opts Options
Codecs []securecookie.Codec
SessionOpts *sessions.Options
}
type xormSession struct {
ID string `xorm:"VARCHAR(400) PK NAME 'id'"`
Data string `xorm:"TEXT"`
CreatedUnix util.TimeStamp `xorm:"created"`
UpdatedUnix util.TimeStamp `xorm:"updated"`
ExpiresUnix util.TimeStamp `xorm:"INDEX"`
tableName string `xorm:"-"` // just to store table name for easier access
}
// Define a type for context keys so that they can't clash with anything else stored in context
type contextKey string
func (xs *xormSession) TableName() string {
return xs.tableName
}
// New creates a new xormstore session
func New(e *xorm.Engine, keyPairs ...[]byte) (*Store, error) {
return NewOptions(e, Options{}, keyPairs...)
}
// NewOptions creates a new xormstore session with options
func NewOptions(e *xorm.Engine, opts Options, keyPairs ...[]byte) (*Store, error) {
st := &Store{
e: e,
opts: opts,
Codecs: securecookie.CodecsFromPairs(keyPairs...),
SessionOpts: &sessions.Options{
Path: defaultPath,
MaxAge: defaultMaxAge,
},
}
if st.opts.TableName == "" {
st.opts.TableName = defaultTableName
}
if !st.opts.SkipCreateTable {
if err := st.e.Sync2(&xormSession{tableName: st.opts.TableName}); err != nil {
return nil, err
}
}
return st, nil
}
// Get returns a session for the given name after adding it to the registry.
func (st *Store) Get(r *http.Request, name string) (*sessions.Session, error) {
return sessions.GetRegistry(r).Get(st, name)
}
// New creates a session with name without adding it to the registry.
func (st *Store) New(r *http.Request, name string) (*sessions.Session, error) {
session := sessions.NewSession(st, name)
opts := *st.SessionOpts
session.Options = &opts
st.MaxAge(st.SessionOpts.MaxAge)
// try fetch from db if there is a cookie
if cookie, err := r.Cookie(name); err == nil {
if err := securecookie.DecodeMulti(name, cookie.Value, &session.ID, st.Codecs...); err != nil {
return session, nil
}
s := &xormSession{tableName: st.opts.TableName}
if has, err := st.e.Where("id = ? AND expires_unix >= ?", session.ID, util.TimeStampNow()).Get(s); !has || err != nil {
return session, nil
}
if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil {
return session, nil
}
context.Set(r, contextKey(name), s)
}
return session, nil
}
// Save session and set cookie header
func (st *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
s, _ := context.Get(r, contextKey(session.Name())).(*xormSession)
// delete if max age is < 0
if session.Options.MaxAge < 0 {
if s != nil {
if _, err := st.e.Delete(&xormSession{
ID: session.ID,
tableName: st.opts.TableName,
}); err != nil {
return err
}
}
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
return nil
}
data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...)
if err != nil {
return err
}
now := util.TimeStampNow()
expire := now.AddDuration(time.Second * time.Duration(session.Options.MaxAge))
if s == nil {
// generate random session ID key suitable for storage in the db
session.ID = strings.TrimRight(
base32.StdEncoding.EncodeToString(
securecookie.GenerateRandomKey(sessionIDLen)), "=")
s = &xormSession{
ID: session.ID,
Data: data,
CreatedUnix: now,
UpdatedUnix: now,
ExpiresUnix: expire,
tableName: st.opts.TableName,
}
if _, err := st.e.Insert(s); err != nil {
return err
}
context.Set(r, contextKey(session.Name()), s)
} else {
s.Data = data
s.UpdatedUnix = now
s.ExpiresUnix = expire
if _, err := st.e.ID(s.ID).Cols("data", "updated_unix", "expires_unix").Update(s); err != nil {
return err
}
}
// set session id cookie
id, err := securecookie.EncodeMulti(session.Name(), session.ID, st.Codecs...)
if err != nil {
return err
}
http.SetCookie(w, sessions.NewCookie(session.Name(), id, session.Options))
return nil
}
// MaxAge sets the maximum age for the store and the underlying cookie
// implementation. Individual sessions can be deleted by setting
// Options.MaxAge = -1 for that session.
func (st *Store) MaxAge(age int) {
st.SessionOpts.MaxAge = age
for _, codec := range st.Codecs {
if sc, ok := codec.(*securecookie.SecureCookie); ok {
sc.MaxAge(age)
}
}
}
// MaxLength restricts the maximum length of new sessions to l.
// If l is 0 there is no limit to the size of a session, use with caution.
// The default is 4096 (default for securecookie)
func (st *Store) MaxLength(l int) {
for _, c := range st.Codecs {
if codec, ok := c.(*securecookie.SecureCookie); ok {
codec.MaxLength(l)
}
}
}
// Cleanup deletes expired sessions
func (st *Store) Cleanup() {
st.e.Where("expires_unix < ?", util.TimeStampNow()).Delete(&xormSession{tableName: st.opts.TableName})
}
// PeriodicCleanup runs Cleanup every interval. Close quit channel to stop.
func (st *Store) PeriodicCleanup(interval time.Duration, quit <-chan struct{}) {
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-t.C:
st.Cleanup()
case <-quit:
return
}
}
}

View File

@ -1,4 +1,6 @@
Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) [![GoDoc](https://godoc.org/github.com/russross/blackfriday?status.svg)](https://godoc.org/github.com/russross/blackfriday)
Blackfriday
[![Build Status][BuildSVG]][BuildURL]
[![Godoc][GodocV2SVG]][GodocV2URL]
===========
Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It
@ -8,7 +10,7 @@ punctuation substitutions, etc.), and it is safe for all utf-8
(unicode) input.
HTML output is currently supported, along with Smartypants
extensions. An experimental LaTeX output engine is also included.
extensions.
It started as a translation from C of [Sundown][3].
@ -16,26 +18,71 @@ It started as a translation from C of [Sundown][3].
Installation
------------
Blackfriday is compatible with Go 1. If you are using an older
release of Go, consider using v1.1 of blackfriday, which was based
on the last stable release of Go prior to Go 1. You can find it as a
tagged commit on github.
Blackfriday is compatible with any modern Go release. With Go and git installed:
With Go 1 and git installed:
go get -u gopkg.in/russross/blackfriday.v2
go get github.com/russross/blackfriday
will download, compile, and install the package into your `$GOPATH` directory
hierarchy.
will download, compile, and install the package into your `$GOPATH`
directory hierarchy. Alternatively, you can achieve the same if you
import it into a project:
import "github.com/russross/blackfriday"
Versions
--------
Currently maintained and recommended version of Blackfriday is `v2`. It's being
developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the
documentation is available at
https://godoc.org/gopkg.in/russross/blackfriday.v2.
It is `go get`-able via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`,
but we highly recommend using package management tool like [dep][7] or
[Glide][8] and make use of semantic versioning. With package management you
should import `github.com/russross/blackfriday` and specify that you're using
version 2.0.0.
Version 2 offers a number of improvements over v1:
* Cleaned up API
* A separate call to [`Parse`][4], which produces an abstract syntax tree for
the document
* Latest bug fixes
* Flexibility to easily add your own rendering extensions
Potential drawbacks:
* Our benchmarks show v2 to be slightly slower than v1. Currently in the
ballpark of around 15%.
* API breakage. If you can't afford modifying your code to adhere to the new API
and don't care too much about the new features, v2 is probably not for you.
* Several bug fixes are trailing behind and still need to be forward-ported to
v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for
tracking.
If you are still interested in the legacy `v1`, you can import it from
`github.com/russross/blackfriday`. Documentation for the legacy v1 can be found
here: https://godoc.org/github.com/russross/blackfriday
### Known issue with `dep`
There is a known problem with using Blackfriday v1 _transitively_ and `dep`.
Currently `dep` prioritizes semver versions over anything else, and picks the
latest one, plus it does not apply a `[[constraint]]` specifier to transitively
pulled in packages. So if you're using something that uses Blackfriday v1, but
that something does not use `dep` yet, you will get Blackfriday v2 pulled in and
your first dependency will fail to build.
There are couple of fixes for it, documented here:
https://github.com/golang/dep/blob/master/docs/FAQ.md#how-do-i-constrain-a-transitive-dependencys-version
Meanwhile, `dep` team is working on a more general solution to the constraints
on transitive dependencies problem: https://github.com/golang/dep/issues/1124.
and `go get` without parameters.
Usage
-----
### v1
For basic usage, it is as simple as getting your input into a byte
slice and calling:
@ -46,34 +93,57 @@ feature set, use this instead:
output := blackfriday.MarkdownCommon(input)
### v2
For the most sensible markdown processing, it is as simple as getting your input
into a byte slice and calling:
```go
output := blackfriday.Run(input)
```
Your input will be parsed and the output rendered with a set of most popular
extensions enabled. If you want the most basic feature set, corresponding with
the bare Markdown specification, use:
```go
output := blackfriday.Run(input, blackfriday.WithNoExtensions())
```
### Sanitize untrusted content
Blackfriday itself does nothing to protect against malicious content. If you are
dealing with user-supplied markdown, we recommend running blackfriday's output
through HTML sanitizer such as
[Bluemonday](https://github.com/microcosm-cc/bluemonday).
dealing with user-supplied markdown, we recommend running Blackfriday's output
through HTML sanitizer such as [Bluemonday][5].
Here's an example of simple usage of blackfriday together with bluemonday:
Here's an example of simple usage of Blackfriday together with Bluemonday:
``` go
```go
import (
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday"
"gopkg.in/russross/blackfriday.v2"
)
// ...
unsafe := blackfriday.MarkdownCommon(input)
unsafe := blackfriday.Run(input)
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
```
### Custom options
### Custom options, v1
If you want to customize the set of options, first get a renderer
(currently either the HTML or LaTeX output engines), then use it to
(currently only the HTML output engine), then use it to
call the more general `Markdown` function. For examples, see the
implementations of `MarkdownBasic` and `MarkdownCommon` in
`markdown.go`.
### Custom options, v2
If you want to customize the set of options, use `blackfriday.WithExtensions`,
`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`.
### `blackfriday-tool`
You can also check out `blackfriday-tool` for a more complete example
of how to use it. Download and install it using:
@ -93,6 +163,22 @@ installed in `$GOPATH/bin`. This is a statically-linked binary that
can be copied to wherever you need it without worrying about
dependencies and library versions.
### Sanitized anchor names
Blackfriday includes an algorithm for creating sanitized anchor names
corresponding to a given input text. This algorithm is used to create
anchors for headings when `EXTENSION_AUTO_HEADER_IDS` is enabled. The
algorithm has a specification, so that other packages can create
compatible anchor names and links to those anchors.
The specification is located at https://godoc.org/github.com/russross/blackfriday#hdr-Sanitized_Anchor_Names.
[`SanitizedAnchorName`](https://godoc.org/github.com/russross/blackfriday#SanitizedAnchorName) exposes this functionality, and can be used to
create compatible links to the anchor names generated by blackfriday.
This algorithm is also implemented in a small standalone package at
[`github.com/shurcooL/sanitized_anchor_name`](https://godoc.org/github.com/shurcooL/sanitized_anchor_name). It can be useful for clients
that want a small package and don't need full functionality of blackfriday.
Features
--------
@ -233,7 +319,7 @@ are a few of note:
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown):
provides a GitHub Flavored Markdown renderer with fenced code block
highlighting, clickable header anchor links.
highlighting, clickable heading anchor links.
It's not customizable, and its goal is to produce HTML output
equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode),
@ -242,27 +328,18 @@ are a few of note:
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
but for markdown.
* LaTeX output: renders output as LaTeX. This is currently part of the
main Blackfriday repository, but may be split into its own project
in the future. If you are interested in owning and maintaining the
LaTeX output component, please be in touch.
It renders some basic documents, but is only experimental at this
point. In particular, it does not do any inline escaping, so input
that happens to look like LaTeX code will be passed through without
modification.
* [Md2Vim](https://github.com/FooSoft/md2vim): transforms markdown files into vimdoc format.
* [LaTeX output](https://bitbucket.org/ambrevar/blackfriday-latex):
renders output as LaTeX.
Todo
TODO
----
* More unit testing
* Improve unicode support. It does not understand all unicode
* Improve Unicode support. It does not understand all Unicode
rules (about what constitutes a letter, a punctuation symbol,
etc.), so it may fail to detect word boundaries correctly in
some instances. It is safe on all utf-8 input.
some instances. It is safe on all UTF-8 input.
License
@ -271,6 +348,16 @@ License
[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt)
[1]: http://daringfireball.net/projects/markdown/ "Markdown"
[2]: http://golang.org/ "Go Language"
[1]: https://daringfireball.net/projects/markdown/ "Markdown"
[2]: https://golang.org/ "Go Language"
[3]: https://github.com/vmg/sundown "Sundown"
[4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func"
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
[6]: https://labix.org/gopkg.in "gopkg.in"
[7]: https://github.com/golang/dep/ "dep"
[8]: https://github.com/Masterminds/glide "Glide"
[BuildSVG]: https://travis-ci.org/russross/blackfriday.svg?branch=master
[BuildURL]: https://travis-ci.org/russross/blackfriday
[GodocV2SVG]: https://godoc.org/gopkg.in/russross/blackfriday.v2?status.svg
[GodocV2URL]: https://godoc.org/gopkg.in/russross/blackfriday.v2

View File

@ -15,8 +15,8 @@ package blackfriday
import (
"bytes"
"github.com/shurcooL/sanitized_anchor_name"
"strings"
"unicode"
)
// Parse block-level data.
@ -93,7 +93,7 @@ func (p *parser) block(out *bytes.Buffer, data []byte) {
// fenced code block:
//
// ``` go
// ``` go info string here
// func fact(n int) int {
// if n <= 1 {
// return n
@ -243,7 +243,7 @@ func (p *parser) prefixHeader(out *bytes.Buffer, data []byte) int {
}
if end > i {
if id == "" && p.flags&EXTENSION_AUTO_HEADER_IDS != 0 {
id = sanitized_anchor_name.Create(string(data[i:end]))
id = SanitizedAnchorName(string(data[i:end]))
}
work := func() bool {
p.inline(out, data[i:end])
@ -563,7 +563,7 @@ func (*parser) isHRule(data []byte) bool {
// and returns the end index if so, or 0 otherwise. It also returns the marker found.
// If syntax is not nil, it gets set to the syntax specified in the fence line.
// A final newline is mandatory to recognize the fence line, unless newlineOptional is true.
func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional bool) (end int, marker string) {
func isFenceLine(data []byte, info *string, oldmarker string, newlineOptional bool) (end int, marker string) {
i, size := 0, 0
// skip up to three spaces
@ -599,9 +599,9 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional
}
// TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here
// into one, always get the syntax, and discard it if the caller doesn't care.
if syntax != nil {
syn := 0
// into one, always get the info string, and discard it if the caller doesn't care.
if info != nil {
infoLength := 0
i = skipChar(data, i, ' ')
if i >= len(data) {
@ -611,14 +611,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional
return 0, ""
}
syntaxStart := i
infoStart := i
if data[i] == '{' {
i++
syntaxStart++
infoStart++
for i < len(data) && data[i] != '}' && data[i] != '\n' {
syn++
infoLength++
i++
}
@ -628,24 +628,24 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional
// strip all whitespace at the beginning and the end
// of the {} block
for syn > 0 && isspace(data[syntaxStart]) {
syntaxStart++
syn--
for infoLength > 0 && isspace(data[infoStart]) {
infoStart++
infoLength--
}
for syn > 0 && isspace(data[syntaxStart+syn-1]) {
syn--
for infoLength > 0 && isspace(data[infoStart+infoLength-1]) {
infoLength--
}
i++
} else {
for i < len(data) && !isspace(data[i]) {
syn++
for i < len(data) && !isverticalspace(data[i]) {
infoLength++
i++
}
}
*syntax = string(data[syntaxStart : syntaxStart+syn])
*info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength]))
}
i = skipChar(data, i, ' ')
@ -663,8 +663,8 @@ func isFenceLine(data []byte, syntax *string, oldmarker string, newlineOptional
// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects.
// If doRender is true, a final newline is mandatory to recognize the fenced code block.
func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool) int {
var syntax string
beg, marker := isFenceLine(data, &syntax, "", false)
var infoString string
beg, marker := isFenceLine(data, &infoString, "", false)
if beg == 0 || beg >= len(data) {
return 0
}
@ -698,7 +698,7 @@ func (p *parser) fencedCodeBlock(out *bytes.Buffer, data []byte, doRender bool)
}
if doRender {
p.r.BlockCode(out, work.Bytes(), syntax)
p.r.BlockCode(out, work.Bytes(), infoString)
}
return beg
@ -1364,7 +1364,7 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int {
id := ""
if p.flags&EXTENSION_AUTO_HEADER_IDS != 0 {
id = sanitized_anchor_name.Create(string(data[prev:eol]))
id = SanitizedAnchorName(string(data[prev:eol]))
}
p.r.Header(out, work, level, id)
@ -1428,3 +1428,24 @@ func (p *parser) paragraph(out *bytes.Buffer, data []byte) int {
p.renderParagraph(out, data[:i])
return i
}
// SanitizedAnchorName returns a sanitized anchor name for the given text.
//
// It implements the algorithm specified in the package comment.
func SanitizedAnchorName(text string) string {
var anchorName []rune
futureDash := false
for _, r := range text {
switch {
case unicode.IsLetter(r) || unicode.IsNumber(r):
if futureDash && len(anchorName) > 0 {
anchorName = append(anchorName, '-')
}
futureDash = false
anchorName = append(anchorName, unicode.ToLower(r))
default:
futureDash = true
}
}
return string(anchorName)
}

32
vendor/github.com/russross/blackfriday/doc.go generated vendored Normal file
View File

@ -0,0 +1,32 @@
// Package blackfriday is a Markdown processor.
//
// It translates plain text with simple formatting rules into HTML or LaTeX.
//
// Sanitized Anchor Names
//
// Blackfriday includes an algorithm for creating sanitized anchor names
// corresponding to a given input text. This algorithm is used to create
// anchors for headings when EXTENSION_AUTO_HEADER_IDS is enabled. The
// algorithm is specified below, so that other packages can create
// compatible anchor names and links to those anchors.
//
// The algorithm iterates over the input text, interpreted as UTF-8,
// one Unicode code point (rune) at a time. All runes that are letters (category L)
// or numbers (category N) are considered valid characters. They are mapped to
// lower case, and included in the output. All other runes are considered
// invalid characters. Invalid characters that preceed the first valid character,
// as well as invalid character that follow the last valid character
// are dropped completely. All other sequences of invalid characters
// between two valid characters are replaced with a single dash character '-'.
//
// SanitizedAnchorName exposes this functionality, and can be used to
// create compatible links to the anchor names generated by blackfriday.
// This algorithm is also implemented in a small standalone package at
// github.com/shurcooL/sanitized_anchor_name. It can be useful for clients
// that want a small package and don't need full functionality of blackfriday.
package blackfriday
// NOTE: Keep Sanitized Anchor Name algorithm in sync with package
// github.com/shurcooL/sanitized_anchor_name.
// Otherwise, users of sanitized_anchor_name will get anchor names
// that are incompatible with those generated by blackfriday.

View File

@ -42,6 +42,7 @@ const (
HTML_SMARTYPANTS_DASHES // enable smart dashes (with HTML_USE_SMARTYPANTS)
HTML_SMARTYPANTS_LATEX_DASHES // enable LaTeX-style dashes (with HTML_USE_SMARTYPANTS and HTML_SMARTYPANTS_DASHES)
HTML_SMARTYPANTS_ANGLED_QUOTES // enable angled double quotes (with HTML_USE_SMARTYPANTS) for double quotes rendering
HTML_SMARTYPANTS_QUOTES_NBSP // enable "French guillemets" (with HTML_USE_SMARTYPANTS)
HTML_FOOTNOTE_RETURN_LINKS // generate a link at the end of a footnote to return to the source
)
@ -254,33 +255,21 @@ func (options *Html) HRule(out *bytes.Buffer) {
out.WriteByte('\n')
}
func (options *Html) BlockCode(out *bytes.Buffer, text []byte, lang string) {
func (options *Html) BlockCode(out *bytes.Buffer, text []byte, info string) {
doubleSpace(out)
// parse out the language names/classes
count := 0
for _, elt := range strings.Fields(lang) {
if elt[0] == '.' {
elt = elt[1:]
}
if len(elt) == 0 {
continue
}
if count == 0 {
out.WriteString("<pre><code class=\"language-")
} else {
out.WriteByte(' ')
}
attrEscape(out, []byte(elt))
count++
endOfLang := strings.IndexAny(info, "\t ")
if endOfLang < 0 {
endOfLang = len(info)
}
if count == 0 {
lang := info[:endOfLang]
if len(lang) == 0 || lang == "." {
out.WriteString("<pre><code>")
} else {
out.WriteString("<pre><code class=\"language-")
attrEscape(out, []byte(lang))
out.WriteString("\">")
}
attrEscape(out, text)
out.WriteString("</code></pre>\n")
}
@ -619,7 +608,7 @@ func (options *Html) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
out.WriteString(`fnref:`)
out.WriteString(options.parameters.FootnoteAnchorPrefix)
out.Write(slug)
out.WriteString(`"><a rel="footnote" href="#`)
out.WriteString(`"><a href="#`)
out.WriteString(`fn:`)
out.WriteString(options.parameters.FootnoteAnchorPrefix)
out.Write(slug)

View File

@ -170,6 +170,10 @@ func lineBreak(p *parser, out *bytes.Buffer, data []byte, offset int) int {
precededByBackslash := offset >= 1 && data[offset-1] == '\\' // see http://spec.commonmark.org/0.18/#example-527
precededByBackslash = precededByBackslash && p.flags&EXTENSION_BACKSLASH_LINE_BREAK != 0
if p.flags&EXTENSION_JOIN_LINES != 0 {
return 1
}
// should there be a hard line break here?
if p.flags&EXTENSION_HARD_LINE_BREAK == 0 && !precededByTwoSpaces && !precededByBackslash {
return 0
@ -484,6 +488,7 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
}
p.notes = append(p.notes, ref)
p.notesRecord[string(ref.link)] = struct{}{}
link = ref.link
title = ref.title
@ -494,9 +499,10 @@ func link(p *parser, out *bytes.Buffer, data []byte, offset int) int {
return 0
}
if t == linkDeferredFootnote {
if t == linkDeferredFootnote && !p.isFootnote(lr) {
lr.noteId = len(p.notes) + 1
p.notes = append(p.notes, lr)
p.notesRecord[string(lr.link)] = struct{}{}
}
// keep link and title from reference

View File

@ -17,6 +17,7 @@ package blackfriday
import (
"bytes"
"strings"
)
// Latex is a type that implements the Renderer interface for LaTeX output.
@ -39,16 +40,17 @@ func (options *Latex) GetFlags() int {
}
// render code chunks using verbatim, or listings if we have a language
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, lang string) {
if lang == "" {
func (options *Latex) BlockCode(out *bytes.Buffer, text []byte, info string) {
if info == "" {
out.WriteString("\n\\begin{verbatim}\n")
} else {
lang := strings.Fields(info)[0]
out.WriteString("\n\\begin{lstlisting}[language=")
out.WriteString(lang)
out.WriteString("]\n")
}
out.Write(text)
if lang == "" {
if info == "" {
out.WriteString("\n\\end{verbatim}\n")
} else {
out.WriteString("\n\\end{lstlisting}\n")

View File

@ -13,9 +13,6 @@
//
//
// Blackfriday markdown processor.
//
// Translates plain text with simple formatting rules into HTML or LaTeX.
package blackfriday
import (
@ -46,6 +43,7 @@ const (
EXTENSION_AUTO_HEADER_IDS // Create the header ID from the text
EXTENSION_BACKSLASH_LINE_BREAK // translate trailing backslashes into line breaks
EXTENSION_DEFINITION_LISTS // render definition lists
EXTENSION_JOIN_LINES // delete newline and join lines
commonHtmlFlags = 0 |
HTML_USE_XHTML |
@ -161,7 +159,7 @@ var blockTags = map[string]struct{}{
// Currently Html and Latex implementations are provided
type Renderer interface {
// block-level callbacks
BlockCode(out *bytes.Buffer, text []byte, lang string)
BlockCode(out *bytes.Buffer, text []byte, infoString string)
BlockQuote(out *bytes.Buffer, text []byte)
BlockHtml(out *bytes.Buffer, text []byte)
Header(out *bytes.Buffer, text func() bool, level int, id string)
@ -220,7 +218,8 @@ type parser struct {
// Footnotes need to be ordered as well as available to quickly check for
// presence. If a ref is also a footnote, it's stored both in refs and here
// in notes. Slice is nil if footnotes not enabled.
notes []*reference
notes []*reference
notesRecord map[string]struct{}
}
func (p *parser) getRef(refid string) (ref *reference, found bool) {
@ -243,6 +242,11 @@ func (p *parser) getRef(refid string) (ref *reference, found bool) {
return ref, found
}
func (p *parser) isFootnote(ref *reference) bool {
_, ok := p.notesRecord[string(ref.link)]
return ok
}
//
//
// Public interface
@ -378,6 +382,7 @@ func MarkdownOptions(input []byte, renderer Renderer, opts Options) []byte {
if extensions&EXTENSION_FOOTNOTES != 0 {
p.notes = make([]*reference, 0)
p.notesRecord = make(map[string]struct{})
}
first := firstPass(p, input)
@ -799,7 +804,17 @@ func ispunct(c byte) bool {
// Test if a character is a whitespace character.
func isspace(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
return ishorizontalspace(c) || isverticalspace(c)
}
// Test if a character is a horizontal whitespace character.
func ishorizontalspace(c byte) bool {
return c == ' ' || c == '\t'
}
// Test if a character is a vertical whitespace character.
func isverticalspace(c byte) bool {
return c == '\n' || c == '\r' || c == '\f' || c == '\v'
}
// Test if a character is letter.

View File

@ -39,7 +39,7 @@ func isdigit(c byte) bool {
return c >= '0' && c <= '9'
}
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool) bool {
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool {
// edge of the buffer is likely to be a tag that we don't get to see,
// so we treat it like text sometimes
@ -96,6 +96,12 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote
*isOpen = false
}
// Note that with the limited lookahead, this non-breaking
// space will also be appended to single double quotes.
if addNBSP && !*isOpen {
out.WriteString("&nbsp;")
}
out.WriteByte('&')
if *isOpen {
out.WriteByte('l')
@ -104,6 +110,11 @@ func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote
}
out.WriteByte(quote)
out.WriteString("quo;")
if addNBSP && *isOpen {
out.WriteString("&nbsp;")
}
return true
}
@ -116,7 +127,7 @@ func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byt
if len(text) >= 3 {
nextChar = text[2]
}
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) {
return 1
}
}
@ -141,7 +152,7 @@ func smartSingleQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byt
if len(text) > 1 {
nextChar = text[1]
}
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote) {
if smartQuoteHelper(out, previousChar, nextChar, 's', &smrt.inSingleQuote, false) {
return 0
}
@ -205,13 +216,13 @@ func smartDashLatex(out *bytes.Buffer, smrt *smartypantsData, previousChar byte,
return 0
}
func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte) int {
func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte, quote byte, addNBSP bool) int {
if bytes.HasPrefix(text, []byte("&quot;")) {
nextChar := byte(0)
if len(text) >= 7 {
nextChar = text[6]
}
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) {
if smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, addNBSP) {
return 5
}
}
@ -224,12 +235,15 @@ func smartAmpVariant(out *bytes.Buffer, smrt *smartypantsData, previousChar byte
return 0
}
func smartAmp(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
return smartAmpVariant(out, smrt, previousChar, text, 'd')
}
func smartAmp(angledQuotes, addNBSP bool) func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
var quote byte = 'd'
if angledQuotes {
quote = 'a'
}
func smartAmpAngledQuote(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
return smartAmpVariant(out, smrt, previousChar, text, 'a')
return func(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
return smartAmpVariant(out, smrt, previousChar, text, quote, addNBSP)
}
}
func smartPeriod(out *bytes.Buffer, smrt *smartypantsData, previousChar byte, text []byte) int {
@ -253,7 +267,7 @@ func smartBacktick(out *bytes.Buffer, smrt *smartypantsData, previousChar byte,
if len(text) >= 3 {
nextChar = text[2]
}
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote) {
if smartQuoteHelper(out, previousChar, nextChar, 'd', &smrt.inDoubleQuote, false) {
return 1
}
}
@ -337,7 +351,7 @@ func smartDoubleQuoteVariant(out *bytes.Buffer, smrt *smartypantsData, previousC
if len(text) > 1 {
nextChar = text[1]
}
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote) {
if !smartQuoteHelper(out, previousChar, nextChar, quote, &smrt.inDoubleQuote, false) {
out.WriteString("&quot;")
}
@ -367,14 +381,30 @@ type smartCallback func(out *bytes.Buffer, smrt *smartypantsData, previousChar b
type smartypantsRenderer [256]smartCallback
var (
smartAmpAngled = smartAmp(true, false)
smartAmpAngledNBSP = smartAmp(true, true)
smartAmpRegular = smartAmp(false, false)
smartAmpRegularNBSP = smartAmp(false, true)
)
func smartypants(flags int) *smartypantsRenderer {
r := new(smartypantsRenderer)
addNBSP := flags&HTML_SMARTYPANTS_QUOTES_NBSP != 0
if flags&HTML_SMARTYPANTS_ANGLED_QUOTES == 0 {
r['"'] = smartDoubleQuote
r['&'] = smartAmp
if !addNBSP {
r['&'] = smartAmpRegular
} else {
r['&'] = smartAmpRegularNBSP
}
} else {
r['"'] = smartAngledDoubleQuote
r['&'] = smartAmpAngledQuote
if !addNBSP {
r['&'] = smartAmpAngled
} else {
r['&'] = smartAmpAngledNBSP
}
}
r['\''] = smartSingleQuote
r['('] = smartParens

18
vendor/vendor.json vendored
View File

@ -647,6 +647,18 @@
"revision": "cb6bfca970f6908083f26f39a79009d608efd5cd",
"revisionTime": "2016-10-16T15:41:25Z"
},
{
"checksumSHA1": "/X7eCdN7MX8zgCjA9s0ktzgTPlA=",
"path": "github.com/lafriks/xormstore",
"revision": "3a80a383a04b29ec2e1bf61279dd948aa809335b",
"revisionTime": "2018-04-09T10:45:24Z"
},
{
"checksumSHA1": "Vxvfs8mukr9GOLSuGIPU4ODyOZc=",
"path": "github.com/lafriks/xormstore/util",
"revision": "c0e2f3dc1ecab3536617967e4b47ee5b9e2ca229",
"revisionTime": "2018-03-11T19:16:53Z"
},
{
"checksumSHA1": "QV4HZTfaXvhD+5PcGM2p+7aCYYI=",
"path": "github.com/lib/pq",
@ -1174,10 +1186,10 @@
"revisionTime": "2016-09-12T16:18:15Z"
},
{
"checksumSHA1": "c7jHQZk5ZEsFR9EXsWJXkszPBZA=",
"checksumSHA1": "Ne3D+KJs1TU2trnDy1UCSwlXbAE=",
"path": "github.com/russross/blackfriday",
"revision": "5f33e7b7878355cd2b7e6b8eefc48a5472c69f70",
"revisionTime": "2016-10-03T16:27:22Z"
"revision": "11635eb403ff09dbc3a6b5a007ab5ab09151c229",
"revisionTime": "2018-04-28T10:25:19Z"
},
{
"checksumSHA1": "zmC8/3V4ls53DJlNTKDZwPSC/dA=",