Signed-off-by: Konrad <konrad@kola-entertainments.de>

# Conflicts:
#	models/migrations/migrations.go
#	models/migrations/v42.go
This commit is contained in:
Konrad Langenberg 2017-09-24 13:20:34 +02:00 committed by Konrad
commit 8b4a48c4bc
80 changed files with 2297 additions and 3629 deletions

View File

@ -16,10 +16,13 @@ pipeline:
TAGS: bindata sqlite
GOPATH: /srv/app
commands:
- apk -U add nodejs nodejs-npm
- npm install
- make clean
- make generate
- make vet
- make lint
- make fmt-check
- make stylesheets-check
- make misspell-check
- make test-vendor
@ -142,7 +145,6 @@ pipeline:
tags: [ '${DRONE_TAG##v}' ]
when:
event: [ tag ]
branch: [ refs/tags/* ]
docker:
image: plugins/docker:17.05
@ -176,7 +178,6 @@ pipeline:
target: /gitea/${DRONE_TAG##v}
when:
event: [ tag ]
branch: [ refs/tags/* ]
release:
image: plugins/s3:1
@ -206,6 +207,17 @@ pipeline:
event: [ push ]
branch: [ master ]
translations:
image: jonasfranz/crowdin
pull: true
secrets: [ crowdin_key ]
project_identifier: gitea
files:
locale_en-US.ini: options/locale/locale_en-US.ini
when:
event: [ push ]
branch: [ master ]
github:
image: plugins/github-release:1
pull: true
@ -213,7 +225,6 @@ pipeline:
- dist/release/*
when:
event: [ tag ]
branch: [ refs/tags/* ]
discord:
image: appleboy/drone-discord:1.0.0

1
.gitignore vendored
View File

@ -58,3 +58,4 @@ coverage.all
/integrations/indexers-sqlite
/integrations/mysql.ini
/integrations/pgsql.ini
/node_modules

View File

@ -97,6 +97,16 @@ and is synced regularily to Crowdin. Once a translation has reached
A SATISFACTORY PERCENTAGE it will be synced back into this repo and
included in the next released version.
## Building Gitea
Generally, the go build tools are installed as-needed in the `Makefile`.
An exception are the tools to build the CSS and images.
- To build CSS: Install [Node.js](https://nodejs.org/en/download/package-manager)
with `npm` and then run `npm install` and `make stylesheets`.
- To build Images: ImageMagick, inkscape and zopflipng binaries must be
available in your `PATH` to run `make generate-images`.
## Code review
Changes to Gitea must be reviewed before they are accepted, no matter who

View File

@ -16,3 +16,4 @@ Patrick G <geek1011@outlook.com> (@geek1011)
Antoine Girard <sapk@sapk.fr> (@sapk)
Lauris Bukšis-Haberkorns <lauris@nix.lv> (@lafriks)
Jonas Östanbäck <jonas.ostanback@gmail.com> (@cez81)
David Schneiderbauer <dschneiderbauer@gmail.com> (@daviian)

View File

@ -15,7 +15,6 @@ else
endif
BINDATA := modules/{options,public,templates}/bindata.go
STYLESHEETS := $(wildcard public/less/index.less public/less/_*.less)
DOCKER_TAG := gitea/gitea:latest
GOFILES := $(shell find . -name "*.go" -type f ! -path "./vendor/*" ! -path "*/bindata.go")
GOFMT ?= gofmt -s
@ -69,11 +68,8 @@ clean:
integrations/indexers-mysql/ integrations/indexers-pgsql integrations/indexers-sqlite \
integrations/mysql.ini integrations/pgsql.ini
required-gofmt-version:
@$(GO) version | grep -q '\(1.7\|1.8\)' || { echo "We require go version 1.7 or 1.8 to format code" >&2 && exit 1; }
.PHONY: fmt
fmt: required-gofmt-version
fmt:
$(GOFMT) -w $(GOFILES)
.PHONY: vet
@ -125,7 +121,7 @@ misspell:
misspell -w -i unknwon $(GOFILES)
.PHONY: fmt-check
fmt-check: required-gofmt-version
fmt-check:
# get all go files and run go fmt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
@ -135,7 +131,7 @@ fmt-check: required-gofmt-version
fi;
.PHONY: test
test: fmt-check
test:
$(GO) test $(PACKAGES)
.PHONY: coverage
@ -302,14 +298,12 @@ stylesheets-check: stylesheets
fi;
.PHONY: stylesheets
stylesheets: public/css/index.css
.IGNORE: public/css/index.css
public/css/index.css: $(STYLESHEETS)
@which lessc > /dev/null; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/kib357/less-go/lessc; \
stylesheets:
@hash minify > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/tdewolff/minify/cmd/minify; \
fi
lessc -i $< -o $@
node_modules/.bin/lessc --no-ie-compat public/less/index.less public/css/index.css
minify -o public/css/index.css public/css/index.css
.PHONY: swagger-ui
swagger-ui:

View File

@ -6,7 +6,6 @@ package integrations
import (
"net/http"
"strings"
"testing"
"code.gitea.io/gitea/models"
@ -33,11 +32,6 @@ func TestAPIUserReposNotLogin(t *testing.T) {
}
}
type searchResponseBody struct {
ok bool
data []api.Repository
}
func TestAPISearchRepoNotLogin(t *testing.T) {
prepareTestEnv(t)
const keyword = "test"
@ -45,10 +39,12 @@ func TestAPISearchRepoNotLogin(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/search?q=%s", keyword)
resp := MakeRequest(t, req, http.StatusOK)
var body searchResponseBody
var body api.SearchResults
DecodeJSON(t, resp, &body)
for _, repo := range body.data {
assert.True(t, strings.Contains(repo.Name, keyword))
assert.NotEmpty(t, body.Data)
for _, repo := range body.Data {
assert.Contains(t, repo.Name, keyword)
assert.False(t, repo.Private)
}
}

View File

@ -0,0 +1 @@
65f1bf27bc3bf70f64657658635e66094edbcb4d

View File

@ -74,6 +74,10 @@ func initIntegrationTest() {
os.Exit(1)
}
setting.AppPath = path.Join(giteaRoot, "gitea")
if _, err := os.Stat(setting.AppPath); err != nil {
fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath)
os.Exit(1)
}
giteaConf := os.Getenv("GITEA_CONF")
if giteaConf == "" {
@ -276,7 +280,7 @@ func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *TestRespo
mac.ServeHTTP(respWriter, req)
if expectedStatus != NoExpectedStatus {
assert.EqualValues(t, expectedStatus, respWriter.HeaderCode,
"Request URL: %s %s", req.URL.String(), buffer.String())
"Request URL: %s", req.URL.String())
}
return &TestResponse{
HeaderCode: respWriter.HeaderCode,

View File

@ -13,6 +13,10 @@ import (
"code.gitea.io/gitea/cmd"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
// register supported doc types
_ "code.gitea.io/gitea/modules/markup/markdown"
_ "code.gitea.io/gitea/modules/markup/orgmode"
"github.com/urfave/cli"
)

View File

@ -46,6 +46,8 @@ const (
ActionReopenIssue // 13
ActionClosePullRequest // 14
ActionReopenPullRequest // 15
ActionDeleteTag // 16
ActionDeleteBranch // 17
)
var (
@ -98,9 +100,8 @@ func (a *Action) AfterSet(colName string, _ xorm.Cell) {
}
// GetOpType gets the ActionType of this action.
// TODO: change return type to ActionType ?
func (a *Action) GetOpType() int {
return int(a.OpType)
func (a *Action) GetOpType() ActionType {
return a.OpType
}
func (a *Action) loadActUser() {
@ -558,6 +559,12 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
// Check it's tag push or branch.
if strings.HasPrefix(opts.RefFullName, git.TagPrefix) {
opType = ActionPushTag
if opts.NewCommitID == git.EmptySHA {
opType = ActionDeleteTag
}
opts.Commits = &PushCommits{}
} else if opts.NewCommitID == git.EmptySHA {
opType = ActionDeleteBranch
opts.Commits = &PushCommits{}
} else {
// if not the first commit, set the compare URL.
@ -603,8 +610,60 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
apiRepo := repo.APIFormat(AccessModeNone)
var shaSum string
var isHookEventPush = false
switch opType {
case ActionCommitRepo: // Push
isHookEventPush = true
if isNewBranch {
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
log.Error(4, "OpenRepository[%s]: %v", repo.RepoPath(), err)
}
shaSum, err = gitRepo.GetBranchCommitID(refName)
if err != nil {
log.Error(4, "GetBranchCommitID[%s]: %v", opts.RefFullName, err)
}
if err = PrepareWebhooks(repo, HookEventCreate, &api.CreatePayload{
Ref: refName,
Sha: shaSum,
RefType: "branch",
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks: %v", err)
}
}
case ActionDeleteBranch: // Delete Branch
isHookEventPush = true
case ActionPushTag: // Create
isHookEventPush = true
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
log.Error(4, "OpenRepository[%s]: %v", repo.RepoPath(), err)
}
shaSum, err = gitRepo.GetTagCommitID(refName)
if err != nil {
log.Error(4, "GetTagCommitID[%s]: %v", opts.RefFullName, err)
}
if err = PrepareWebhooks(repo, HookEventCreate, &api.CreatePayload{
Ref: refName,
Sha: shaSum,
RefType: "tag",
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks: %v", err)
}
case ActionDeleteTag: // Delete Tag
isHookEventPush = true
}
if isHookEventPush {
if err = PrepareWebhooks(repo, HookEventPush, &api.PushPayload{
Ref: opts.RefFullName,
Before: opts.OldCommitID,
@ -617,41 +676,6 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
}); err != nil {
return fmt.Errorf("PrepareWebhooks: %v", err)
}
if isNewBranch {
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
log.Error(4, "OpenRepository[%s]: %v", repo.RepoPath(), err)
}
shaSum, err = gitRepo.GetBranchCommitID(refName)
if err != nil {
log.Error(4, "GetBranchCommitID[%s]: %v", opts.RefFullName, err)
}
return PrepareWebhooks(repo, HookEventCreate, &api.CreatePayload{
Ref: refName,
Sha: shaSum,
RefType: "branch",
Repo: apiRepo,
Sender: apiPusher,
})
}
case ActionPushTag: // Create
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
log.Error(4, "OpenRepository[%s]: %v", repo.RepoPath(), err)
}
shaSum, err = gitRepo.GetTagCommitID(refName)
if err != nil {
log.Error(4, "GetTagCommitID[%s]: %v", opts.RefFullName, err)
}
return PrepareWebhooks(repo, HookEventCreate, &api.CreatePayload{
Ref: refName,
Sha: shaSum,
RefType: "tag",
Repo: apiRepo,
Sender: apiPusher,
})
}
return nil

View File

@ -5,6 +5,7 @@ import (
"strings"
"testing"
"code.gitea.io/git"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@ -202,57 +203,118 @@ func TestUpdateIssuesCommit(t *testing.T) {
CheckConsistencyFor(t, &Action{})
}
func TestCommitRepoAction(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
repo := AssertExistsAndLoadBean(t, &Repository{ID: 2, OwnerID: user.ID}).(*Repository)
repo.Owner = user
pushCommits := NewPushCommits()
pushCommits.Commits = []*PushCommit{
{
Sha1: "abcdef1",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user4@example.com",
AuthorName: "User Four",
Message: "message1",
},
{
Sha1: "abcdef2",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "message2",
},
}
pushCommits.Len = len(pushCommits.Commits)
actionBean := &Action{
OpType: ActionCommitRepo,
ActUserID: user.ID,
ActUser: user,
RepoID: repo.ID,
Repo: repo,
RefName: "refName",
IsPrivate: repo.IsPrivate,
}
func testCorrectRepoAction(t *testing.T, opts CommitRepoActionOptions, actionBean *Action) {
AssertNotExistsBean(t, actionBean)
assert.NoError(t, CommitRepoAction(CommitRepoActionOptions{
PusherName: user.Name,
RepoOwnerID: user.ID,
RepoName: repo.Name,
RefFullName: "refName",
OldCommitID: "oldCommitID",
NewCommitID: "newCommitID",
Commits: pushCommits,
}))
assert.NoError(t, CommitRepoAction(opts))
AssertExistsAndLoadBean(t, actionBean)
CheckConsistencyFor(t, &Action{})
}
func TestCommitRepoAction(t *testing.T) {
samples := []struct {
userID int64
repositoryID int64
commitRepoActionOptions CommitRepoActionOptions
action Action
}{
{
userID: 2,
repositoryID: 2,
commitRepoActionOptions: CommitRepoActionOptions{
RefFullName: "refName",
OldCommitID: "oldCommitID",
NewCommitID: "newCommitID",
Commits: &PushCommits{
avatars: make(map[string]string),
Commits: []*PushCommit{
{
Sha1: "abcdef1",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user4@example.com",
AuthorName: "User Four",
Message: "message1",
},
{
Sha1: "abcdef2",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "message2",
},
},
Len: 2,
},
},
action: Action{
OpType: ActionCommitRepo,
RefName: "refName",
},
},
{
userID: 2,
repositoryID: 1,
commitRepoActionOptions: CommitRepoActionOptions{
RefFullName: git.TagPrefix + "v1.1",
OldCommitID: git.EmptySHA,
NewCommitID: "newCommitID",
Commits: &PushCommits{},
},
action: Action{
OpType: ActionPushTag,
RefName: "v1.1",
},
},
{
userID: 2,
repositoryID: 1,
commitRepoActionOptions: CommitRepoActionOptions{
RefFullName: git.TagPrefix + "v1.1",
OldCommitID: "oldCommitID",
NewCommitID: git.EmptySHA,
Commits: &PushCommits{},
},
action: Action{
OpType: ActionDeleteTag,
RefName: "v1.1",
},
},
{
userID: 2,
repositoryID: 1,
commitRepoActionOptions: CommitRepoActionOptions{
RefFullName: git.BranchPrefix + "feature/1",
OldCommitID: "oldCommitID",
NewCommitID: git.EmptySHA,
Commits: &PushCommits{},
},
action: Action{
OpType: ActionDeleteBranch,
RefName: "feature/1",
},
},
}
for _, s := range samples {
prepareTestEnv(t)
user := AssertExistsAndLoadBean(t, &User{ID: s.userID}).(*User)
repo := AssertExistsAndLoadBean(t, &Repository{ID: s.repositoryID, OwnerID: user.ID}).(*Repository)
repo.Owner = user
s.commitRepoActionOptions.PusherName = user.Name
s.commitRepoActionOptions.RepoOwnerID = user.ID
s.commitRepoActionOptions.RepoName = repo.Name
s.action.ActUserID = user.ID
s.action.RepoID = repo.ID
s.action.IsPrivate = repo.IsPrivate
testCorrectRepoAction(t, s.commitRepoActionOptions, &s.action)
}
}
func TestTransferRepoAction(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())

View File

@ -26,6 +26,7 @@ type ProtectedBranch struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s)"`
BranchName string `xorm:"UNIQUE(s)"`
CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableWhitelist bool
WhitelistUserIDs []int64 `xorm:"JSON TEXT"`
WhitelistTeamIDs []int64 `xorm:"JSON TEXT"`

View File

@ -15,3 +15,27 @@
user_id: 4
repo_id: 3
mode: 2 # write
-
id: 4
user_id: 15
repo_id: 22
mode: 2 # write
-
id: 5
user_id: 15
repo_id: 21
mode: 2 # write
-
id: 6
user_id: 15
repo_id: 23
mode: 4 # owner
-
id: 7
user_id: 15
repo_id: 24
mode: 4 # owner

View File

@ -29,3 +29,11 @@
is_public: false
is_owner: true
num_teams: 1
-
id: 5
uid: 15
org_id: 17
is_public: true
is_owner: true
num_teams: 1

View File

@ -188,3 +188,100 @@
num_pulls: 0
num_closed_pulls: 0
num_watches: 0
-
id: 17
owner_id: 15
lower_name: big_test_public_1
name: big_test_public_1
is_private: false
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
num_watches: 0
is_mirror: false
-
id: 18
owner_id: 15
lower_name: big_test_public_2
name: big_test_public_2
is_private: false
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
-
id: 19
owner_id: 15
lower_name: big_test_private_1
name: big_test_private_1
is_private: true
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
-
id: 20
owner_id: 15
lower_name: big_test_private_2
name: big_test_private_2
is_private: true
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
-
id: 21
owner_id: 16
lower_name: big_test_public_3
name: big_test_public_3
is_private: false
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
-
id: 22
owner_id: 16
lower_name: big_test_private_3
name: big_test_private_3
is_private: true
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
-
id: 23
owner_id: 17
lower_name: big_test_public_4
name: big_test_public_4
is_private: false
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
is_mirror: false
-
id: 24
owner_id: 17
lower_name: big_test_private_4
name: big_test_private_4
is_private: true
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
is_mirror: false

View File

@ -37,3 +37,12 @@
num_repos: 0
num_members: 1
unit_types: '[1,2,3,4,5,6,7]'
-
id: 5
org_id: 17
lower_name: owners
name: Owners
authorize: 4 # owner
num_repos: 2
num_members: 1
unit_types: '[1,2,3,4,5,6,7]'

View File

@ -15,3 +15,15 @@
org_id: 3
team_id: 1
repo_id: 5
-
id: 4
org_id: 17
team_id: 5
repo_id: 23
-
id: 5
org_id: 17
team_id: 5
repo_id: 24

View File

@ -27,3 +27,9 @@
org_id: 7
team_id: 4
uid: 5
-
id: 6
org_id: 17
team_id: 5
uid: 15

View File

@ -218,3 +218,50 @@
avatar_email: user13@example.com
num_repos: 3
is_active: true
-
id: 15
lower_name: user15
name: user15
full_name: User 15
email: user15@example.com
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
is_admin: false
avatar: avatar15
avatar_email: user15@example.com
num_repos: 4
is_active: true
-
id: 16
lower_name: user16
name: user16
full_name: User 16
email: user16@example.com
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 0 # individual
salt: ZogKvWdyEx
is_admin: false
avatar: avatar16
avatar_email: user16@example.com
num_repos: 2
is_active: true
-
id: 17
lower_name: user17
name: user17
full_name: User 17
email: user17@example.com
passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
type: 1 # organization
salt: ZogKvWdyEx
is_admin: false
avatar: avatar17
avatar_email: user17@example.com
num_repos: 2
is_active: true
num_members: 1
num_teams: 1

View File

@ -282,11 +282,7 @@ func DeleteGPGKey(doer *User, id int64) (err error) {
return err
}
if err = sess.Commit(); err != nil {
return err
}
return nil
return sess.Commit()
}
// CommitVerification represents a commit validation of signature

View File

@ -571,11 +571,7 @@ func (issue *Issue) ReadBy(userID int64) error {
return err
}
if err := setNotificationStatusReadIfUnread(x, userID, issue.ID); err != nil {
return err
}
return nil
return setNotificationStatusReadIfUnread(x, userID, issue.ID)
}
func updateIssueCols(e Engine, issue *Issue, cols ...string) error {

View File

@ -498,10 +498,7 @@ func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error {
}
if ok, _ := c.Extension("AUTH"); ok {
if err = c.Auth(a); err != nil {
return err
}
return nil
return c.Auth(a)
}
return ErrUnsupportedLoginType
}

View File

@ -13,8 +13,8 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/mailer"
"code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"gopkg.in/gomail.v2"
"gopkg.in/macaron.v1"
@ -167,7 +167,7 @@ func composeIssueCommentMessage(issue *Issue, doer *User, comment *Comment, tplN
log.Error(3, "Template: %v", err)
}
msg := mailer.NewMessageFrom(tos, fmt.Sprintf(`"%s" <%s>`, doer.DisplayName(), setting.MailService.FromEmail), subject, content.String())
msg := mailer.NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, content.String())
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
return msg
}

View File

@ -133,6 +133,10 @@ var migrations = []Migration{
// v41 -> v42
NewMigration("add default value to user prohibit_login", addDefaultValueToUserProhibitLogin),
// v42 -> v43
NewMigration("add tags to releases and sync existing repositories", releaseAddColumnIsTagAndSyncTags),
// v43 -> v44
NewMigration("fix protected branch can push value to false", fixProtectedBranchCanPushValue),
// v44 -> v45
NewMigration("add issue_dependency table", addIssueDependencyTables),
}

View File

@ -119,9 +119,5 @@ func addUnitsToTables(x *xorm.Engine) error {
}
}
if err := sess.Commit(); err != nil {
return err
}
return nil
return sess.Commit()
}

View File

@ -51,8 +51,5 @@ func useNewPublickeyFormat(x *xorm.Engine) error {
}
f.Close()
if err = os.Rename(tmpPath, fpath); err != nil {
return err
}
return nil
return os.Rename(tmpPath, fpath)
}

View File

@ -7,18 +7,51 @@ package migrations
import (
"fmt"
"code.gitea.io/git"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"github.com/go-xorm/xorm"
)
func addIssueDependencyTables(x *xorm.Engine) (err error) {
// ReleaseV39 describes the added field for Release
type ReleaseV39 struct {
IsTag bool `xorm:"NOT NULL DEFAULT false"`
}
err = x.Sync(new(models.IssueDependency))
// TableName will be invoked by XORM to customrize the table name
func (*ReleaseV39) TableName() string {
return "release"
}
if err != nil {
return fmt.Errorf("Error creating issue_dependency_table column definition: %v", err)
func releaseAddColumnIsTagAndSyncTags(x *xorm.Engine) error {
if err := x.Sync2(new(ReleaseV39)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
return err
// For the sake of SQLite3, we can't use x.Iterate here.
offset := 0
pageSize := 20
for {
repos := make([]*models.Repository, 0, pageSize)
if err := x.Table("repository").Asc("id").Limit(pageSize, offset).Find(&repos); err != nil {
return fmt.Errorf("select repos [offset: %d]: %v", offset, err)
}
for _, repo := range repos {
gitRepo, err := git.OpenRepository(repo.RepoPath())
if err != nil {
log.Warn("OpenRepository: %v", err)
continue
}
if err = models.SyncReleasesWithTags(repo, gitRepo); err != nil {
log.Warn("SyncReleasesWithTags: %v", err)
}
}
if len(repos) < pageSize {
break
}
offset += pageSize
}
return nil
}

26
models/migrations/v43.go Normal file
View File

@ -0,0 +1,26 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package migrations
import (
"fmt"
"github.com/go-xorm/xorm"
)
func fixProtectedBranchCanPushValue(x *xorm.Engine) error {
type ProtectedBranch struct {
CanPush bool `xorm:"NOT NULL DEFAULT false"`
}
if err := x.Sync2(new(ProtectedBranch)); err != nil {
return fmt.Errorf("Sync2: %v", err)
}
_, err := x.Cols("can_push").Update(&ProtectedBranch{
CanPush: false,
})
return err
}

1
models/migrations/v44.go Normal file
View File

@ -0,0 +1 @@
package migrations

View File

@ -235,11 +235,7 @@ func DeleteOrganization(org *User) (err error) {
}
}
if err = sess.Commit(); err != nil {
return err
}
return nil
return sess.Commit()
}
func deleteOrg(e *xorm.Session, u *User) error {

View File

@ -252,7 +252,7 @@ func TestOrganizations(t *testing.T) {
[]int64{3, 6})
testSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 2, PageSize: 2},
[]int64{7})
[]int64{7, 17})
testSuccess(&SearchUserOptions{Page: 3, PageSize: 2},
[]int64{})

View File

@ -34,7 +34,8 @@ type Release struct {
NumCommitsBehind int64 `xorm:"-"`
Note string `xorm:"TEXT"`
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
IsPrerelease bool
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
IsTag bool `xorm:"NOT NULL DEFAULT false"`
Attachments []*Attachment `xorm:"-"`
@ -139,17 +140,18 @@ func createTag(gitRepo *git.Repository, rel *Release) error {
}
return err
}
} else {
commit, err := gitRepo.GetTagCommit(rel.TagName)
if err != nil {
return fmt.Errorf("GetTagCommit: %v", err)
}
rel.LowerTagName = strings.ToLower(rel.TagName)
}
commit, err := gitRepo.GetTagCommit(rel.TagName)
if err != nil {
return fmt.Errorf("GetTagCommit: %v", err)
}
rel.Sha1 = commit.ID.String()
rel.NumCommits, err = commit.CommitsCount()
if err != nil {
return fmt.Errorf("CommitsCount: %v", err)
}
rel.Sha1 = commit.ID.String()
rel.CreatedUnix = commit.Author.When.Unix()
rel.NumCommits, err = commit.CommitsCount()
if err != nil {
return fmt.Errorf("CommitsCount: %v", err)
}
}
return nil
@ -236,6 +238,7 @@ func GetReleaseByID(id int64) (*Release, error) {
// FindReleasesOptions describes the conditions to Find releases
type FindReleasesOptions struct {
IncludeDrafts bool
IncludeTags bool
TagNames []string
}
@ -246,6 +249,9 @@ func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
if !opts.IncludeDrafts {
cond = cond.And(builder.Eq{"is_draft": false})
}
if !opts.IncludeTags {
cond = cond.And(builder.Eq{"is_tag": false})
}
if len(opts.TagNames) > 0 {
cond = cond.And(builder.In("tag_name", opts.TagNames))
}
@ -361,6 +367,8 @@ func UpdateRelease(gitRepo *git.Repository, rel *Release, attachmentUUIDs []stri
if err = createTag(gitRepo, rel); err != nil {
return err
}
rel.LowerTagName = strings.ToLower(rel.TagName)
_, err = x.Id(rel.ID).AllCols().Update(rel)
if err != nil {
return err
@ -397,11 +405,64 @@ func DeleteReleaseByID(id int64, u *User, delTag bool) error {
if err != nil && !strings.Contains(stderr, "not found") {
return fmt.Errorf("git tag -d: %v - %s", err, stderr)
}
}
if _, err = x.Id(rel.ID).Delete(new(Release)); err != nil {
return fmt.Errorf("Delete: %v", err)
if _, err = x.Id(rel.ID).Delete(new(Release)); err != nil {
return fmt.Errorf("Delete: %v", err)
}
} else {
rel.IsTag = true
rel.IsDraft = false
rel.IsPrerelease = false
rel.Title = ""
rel.Note = ""
if _, err = x.Id(rel.ID).AllCols().Update(rel); err != nil {
return fmt.Errorf("Update: %v", err)
}
}
return nil
}
// SyncReleasesWithTags synchronizes release table with repository tags
func SyncReleasesWithTags(repo *Repository, gitRepo *git.Repository) error {
existingRelTags := make(map[string]struct{})
opts := FindReleasesOptions{IncludeDrafts: true, IncludeTags: true}
for page := 1; ; page++ {
rels, err := GetReleasesByRepoID(repo.ID, opts, page, 100)
if err != nil {
return fmt.Errorf("GetReleasesByRepoID: %v", err)
}
if len(rels) == 0 {
break
}
for _, rel := range rels {
if rel.IsDraft {
continue
}
commitID, err := gitRepo.GetTagCommitID(rel.TagName)
if err != nil {
return fmt.Errorf("GetTagCommitID: %v", err)
}
if !gitRepo.IsTagExist(rel.TagName) || commitID != rel.Sha1 {
if err := pushUpdateDeleteTag(repo, gitRepo, rel.TagName); err != nil {
return fmt.Errorf("pushUpdateDeleteTag: %v", err)
}
} else {
existingRelTags[strings.ToLower(rel.TagName)] = struct{}{}
}
}
}
tags, err := gitRepo.GetTags()
if err != nil {
return fmt.Errorf("GetTags: %v", err)
}
for _, tagName := range tags {
if _, ok := existingRelTags[strings.ToLower(tagName)]; !ok {
if err := pushUpdateAddTag(repo, gitRepo, tagName); err != nil {
return fmt.Errorf("pushUpdateAddTag: %v", err)
}
}
}
return nil
}

View File

@ -639,7 +639,7 @@ func (repo *Repository) UpdateSize() error {
// CanBeForked returns true if repository meets the requirements of being forked.
func (repo *Repository) CanBeForked() bool {
return !repo.IsBare
return !repo.IsBare && repo.UnitEnabled(UnitTypeCode)
}
// CanEnablePulls returns true if repository meets the requirements of accepting pulls.
@ -951,6 +951,10 @@ func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, err
if headBranch != nil {
repo.DefaultBranch = headBranch.Name
}
if err = SyncReleasesWithTags(repo, gitRepo); err != nil {
log.Error(4, "Failed to synchronize tags to releases for repository: %v", err)
}
}
if err = repo.UpdateSize(); err != nil {

View File

@ -97,14 +97,14 @@ type SearchRepoOptions struct {
// Owner in we search search
//
// in: query
OwnerID int64 `json:"uid"`
Searcher *User `json:"-"` //ID of the person who's seeking
OrderBy string `json:"-"`
Private bool `json:"-"` // Include private repositories in results
Collaborate bool `json:"-"` // Include collaborative repositories
Starred bool `json:"-"`
Page int `json:"-"`
IsProfile bool `json:"-"`
OwnerID int64 `json:"uid"`
Searcher *User `json:"-"` //ID of the person who's seeking
OrderBy SearchOrderBy `json:"-"`
Private bool `json:"-"` // Include private repositories in results
Collaborate bool `json:"-"` // Include collaborative repositories
Starred bool `json:"-"`
Page int `json:"-"`
IsProfile bool `json:"-"`
// Limit of result
//
// maximum: setting.ExplorePagingNum
@ -112,6 +112,25 @@ type SearchRepoOptions struct {
PageSize int `json:"limit"` // Can be smaller than or equal to setting.ExplorePagingNum
}
//SearchOrderBy is used to sort the result
type SearchOrderBy string
func (s SearchOrderBy) String() string {
return string(s)
}
// Strings for sorting result
const (
SearchOrderByAlphabetically SearchOrderBy = "name ASC"
SearchOrderByAlphabeticallyReverse = "name DESC"
SearchOrderByLeastUpdated = "updated_unix ASC"
SearchOrderByRecentUpdated = "updated_unix DESC"
SearchOrderByOldest = "created_unix ASC"
SearchOrderByNewest = "created_unix DESC"
SearchOrderBySize = "size ASC"
SearchOrderBySizeReverse = "size DESC"
)
// SearchRepositoryByName takes keyword and part of repository name to search,
// it returns results in given range and number of total results.
func SearchRepositoryByName(opts *SearchRepoOptions) (repos RepositoryList, count int64, err error) {
@ -164,7 +183,7 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (repos RepositoryList, coun
}
if len(opts.OrderBy) == 0 {
opts.OrderBy = "name ASC"
opts.OrderBy = SearchOrderByAlphabetically
}
sess := x.NewSession()
@ -193,7 +212,7 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (repos RepositoryList, coun
if err = sess.
Where(cond).
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
OrderBy(opts.OrderBy).
OrderBy(opts.OrderBy.String()).
Find(&repos); err != nil {
return nil, 0, fmt.Errorf("Repo: %v", err)
}
@ -217,7 +236,7 @@ func Repositories(opts *SearchRepoOptions) (_ RepositoryList, count int64, err e
if err = x.
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
OrderBy(opts.OrderBy).
OrderBy(opts.OrderBy.String()).
Find(&repos); err != nil {
return nil, 0, fmt.Errorf("Repo: %v", err)
}
@ -236,7 +255,7 @@ func GetRecentUpdatedRepositories(opts *SearchRepoOptions) (repos RepositoryList
var cond = builder.NewCond()
if len(opts.OrderBy) == 0 {
opts.OrderBy = "updated_unix DESC"
opts.OrderBy = SearchOrderByRecentUpdated
}
if !opts.Private {
@ -270,7 +289,7 @@ func GetRecentUpdatedRepositories(opts *SearchRepoOptions) (repos RepositoryList
if err = x.Where(cond).
Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
Limit(opts.PageSize).
OrderBy(opts.OrderBy).
OrderBy(opts.OrderBy.String()).
Find(&repos); err != nil {
return nil, 0, fmt.Errorf("Repo: %v", err)
}

View File

@ -13,6 +13,7 @@ import (
"github.com/go-xorm/xorm"
"gopkg.in/ini.v1"
"code.gitea.io/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
@ -156,6 +157,15 @@ func (m *Mirror) runSync() bool {
return false
}
gitRepo, err := git.OpenRepository(repoPath)
if err != nil {
log.Error(4, "OpenRepository: %v", err)
return false
}
if err = SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
log.Error(4, "Failed to synchronize tags to releases for repository: %v", err)
}
if err := m.Repo.UpdateSize(); err != nil {
log.Error(4, "Failed to update size for mirror repository: %v", err)
}

View File

@ -599,11 +599,7 @@ func RewriteAllPublicKeys() error {
defer f.Close()
}
if err = os.Rename(tmpPath, fPath); err != nil {
return err
}
return nil
return os.Rename(tmpPath, fPath)
}
// ________ .__ ____ __.

View File

@ -5,8 +5,12 @@
package models
import (
"os"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/com"
"github.com/go-xorm/core"
"github.com/go-xorm/xorm"
"github.com/stretchr/testify/assert"
@ -38,6 +42,12 @@ func PrepareTestDatabase() error {
return LoadFixtures()
}
func prepareTestEnv(t testing.TB) {
assert.NoError(t, PrepareTestDatabase())
assert.NoError(t, os.RemoveAll(setting.RepoRootPath))
assert.NoError(t, com.CopyDir("../integrations/gitea-repositories-meta", setting.RepoRootPath))
}
type testCond struct {
query interface{}
args []interface{}

View File

@ -81,6 +81,93 @@ func PushUpdate(branch string, opt PushUpdateOptions) error {
return nil
}
func pushUpdateDeleteTag(repo *Repository, gitRepo *git.Repository, tagName string) error {
rel, err := GetRelease(repo.ID, tagName)
if err != nil {
if IsErrReleaseNotExist(err) {
return nil
}
return fmt.Errorf("GetRelease: %v", err)
}
if rel.IsTag {
if _, err = x.Id(rel.ID).Delete(new(Release)); err != nil {
return fmt.Errorf("Delete: %v", err)
}
} else {
rel.IsDraft = true
rel.NumCommits = 0
rel.Sha1 = ""
if _, err = x.Id(rel.ID).AllCols().Update(rel); err != nil {
return fmt.Errorf("Update: %v", err)
}
}
return nil
}
func pushUpdateAddTag(repo *Repository, gitRepo *git.Repository, tagName string) error {
rel, err := GetRelease(repo.ID, tagName)
if err != nil && !IsErrReleaseNotExist(err) {
return fmt.Errorf("GetRelease: %v", err)
}
tag, err := gitRepo.GetTag(tagName)
if err != nil {
return fmt.Errorf("GetTag: %v", err)
}
commit, err := tag.Commit()
if err != nil {
return fmt.Errorf("Commit: %v", err)
}
tagCreatedUnix := commit.Author.When.Unix()
author, err := GetUserByEmail(commit.Author.Email)
if err != nil && !IsErrUserNotExist(err) {
return fmt.Errorf("GetUserByEmail: %v", err)
}
commitsCount, err := commit.CommitsCount()
if err != nil {
return fmt.Errorf("CommitsCount: %v", err)
}
if rel == nil {
rel = &Release{
RepoID: repo.ID,
Title: "",
TagName: tagName,
LowerTagName: strings.ToLower(tagName),
Target: "",
Sha1: commit.ID.String(),
NumCommits: commitsCount,
Note: "",
IsDraft: false,
IsPrerelease: false,
IsTag: true,
CreatedUnix: tagCreatedUnix,
}
if author != nil {
rel.PublisherID = author.ID
}
if _, err = x.InsertOne(rel); err != nil {
return fmt.Errorf("InsertOne: %v", err)
}
} else {
rel.Sha1 = commit.ID.String()
rel.CreatedUnix = tagCreatedUnix
rel.NumCommits = commitsCount
rel.IsDraft = false
if rel.IsTag && author != nil {
rel.PublisherID = author.ID
}
if _, err = x.Id(rel.ID).AllCols().Update(rel); err != nil {
return fmt.Errorf("Update: %v", err)
}
}
return nil
}
func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
isNewRef := opts.OldCommitID == git.EmptySHA
isDelRef := opts.NewCommitID == git.EmptySHA
@ -106,12 +193,6 @@ func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
return nil, fmt.Errorf("GetRepositoryByName: %v", err)
}
if isDelRef {
log.GitLogger.Info("Reference '%s' has been deleted from '%s/%s' by %s",
opts.RefFullName, opts.RepoUserName, opts.RepoName, opts.PusherName)
return repo, nil
}
gitRepo, err := git.OpenRepository(repoPath)
if err != nil {
return nil, fmt.Errorf("OpenRepository: %v", err)
@ -121,39 +202,42 @@ func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
log.Error(4, "Failed to update size for repository: %v", err)
}
// Push tags.
var commits = &PushCommits{}
if strings.HasPrefix(opts.RefFullName, git.TagPrefix) {
if err := CommitRepoAction(CommitRepoActionOptions{
PusherName: opts.PusherName,
RepoOwnerID: owner.ID,
RepoName: repo.Name,
RefFullName: opts.RefFullName,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: &PushCommits{},
}); err != nil {
return nil, fmt.Errorf("CommitRepoAction (tag): %v", err)
// If is tag reference
if isDelRef {
err = pushUpdateDeleteTag(repo, gitRepo, opts.RefFullName[len(git.TagPrefix):])
if err != nil {
return nil, fmt.Errorf("pushUpdateDeleteTag: %v", err)
}
} else {
err = pushUpdateAddTag(repo, gitRepo, opts.RefFullName[len(git.TagPrefix):])
if err != nil {
return nil, fmt.Errorf("pushUpdateAddTag: %v", err)
}
}
return repo, nil
}
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil {
return nil, fmt.Errorf("gitRepo.GetCommit: %v", err)
}
// Push new branch.
var l *list.List
if isNewRef {
l, err = newCommit.CommitsBeforeLimit(10)
} else if !isDelRef {
// If is branch reference
newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeLimit: %v", err)
return nil, fmt.Errorf("gitRepo.GetCommit: %v", err)
}
} else {
l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeUntil: %v", err)
// Push new branch.
var l *list.List
if isNewRef {
l, err = newCommit.CommitsBeforeLimit(10)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeLimit: %v", err)
}
} else {
l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeUntil: %v", err)
}
}
commits = ListToPushCommits(l)
}
if err := CommitRepoAction(CommitRepoActionOptions{
@ -163,9 +247,9 @@ func pushUpdate(opts PushUpdateOptions) (repo *Repository, err error) {
RefFullName: opts.RefFullName,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
Commits: ListToPushCommits(l),
Commits: commits,
}); err != nil {
return nil, fmt.Errorf("CommitRepoAction (branch): %v", err)
return nil, fmt.Errorf("CommitRepoAction: %v", err)
}
return repo, nil
}

View File

@ -0,0 +1,89 @@
// Copyright 2017 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 base
import (
"math/big"
"unicode/utf8"
)
// NaturalSortLess compares two strings so that they could be sorted in natural order
func NaturalSortLess(s1, s2 string) bool {
var i1, i2 int
for {
rune1, j1, end1 := getNextRune(s1, i1)
rune2, j2, end2 := getNextRune(s2, i2)
if end1 || end2 {
return end1 != end2 && end1
}
dec1 := isDecimal(rune1)
dec2 := isDecimal(rune2)
var less, equal bool
if dec1 && dec2 {
i1, i2, less, equal = compareByNumbers(s1, i1, s2, i2)
} else if !dec1 && !dec2 {
equal = rune1 == rune2
less = rune1 < rune2
i1 = j1
i2 = j2
} else {
return rune1 < rune2
}
if !equal {
return less
}
}
}
func getNextRune(str string, pos int) (rune, int, bool) {
if pos < len(str) {
r, w := utf8.DecodeRuneInString(str[pos:])
// Fallback to ascii
if r == utf8.RuneError {
r = rune(str[pos])
w = 1
}
return r, pos + w, false
}
return 0, pos, true
}
func isDecimal(r rune) bool {
return '0' <= r && r <= '9'
}
func compareByNumbers(str1 string, pos1 int, str2 string, pos2 int) (i1, i2 int, less, equal bool) {
var d1, d2 bool = true, true
var dec1, dec2 string
for d1 || d2 {
if d1 {
r, j, end := getNextRune(str1, pos1)
if !end && isDecimal(r) {
dec1 += string(r)
pos1 = j
} else {
d1 = false
}
}
if d2 {
r, j, end := getNextRune(str2, pos2)
if !end && isDecimal(r) {
dec2 += string(r)
pos2 = j
} else {
d2 = false
}
}
}
less, equal = compareBigNumbers(dec1, dec2)
return pos1, pos2, less, equal
}
func compareBigNumbers(dec1, dec2 string) (less, equal bool) {
d1, _ := big.NewInt(0).SetString(dec1, 10)
d2, _ := big.NewInt(0).SetString(dec2, 10)
cmp := d1.Cmp(d2)
return cmp < 0, cmp == 0
}

View File

@ -0,0 +1,24 @@
// Copyright 2017 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 base
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNaturalSortLess(t *testing.T) {
test := func(s1, s2 string, less bool) {
assert.Equal(t, less, NaturalSortLess(s1, s2))
}
test("v1.20.0", "v1.2.0", false)
test("v1.20.0", "v1.29.0", true)
test("v1.20.0", "v1.20.0", false)
test("abc", "bcd", "abc" < "bcd")
test("a-1-a", "a-1-b", true)
test("2", "12", true)
test("a", "ab", "a" < "ab")
}

View File

@ -134,21 +134,23 @@ func RetrieveBaseRepo(ctx *Context, repo *models.Repository) {
}
}
// composeGoGetImport returns go-get-import meta content.
func composeGoGetImport(owner, repo string) string {
// ComposeGoGetImport returns go-get-import meta content.
func ComposeGoGetImport(owner, repo string) string {
return path.Join(setting.Domain, setting.AppSubURL, owner, repo)
}
// earlyResponseForGoGetMeta responses appropriate go-get meta with status 200
// EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200
// if user does not have actual access to the requested repository,
// or the owner or repository does not exist at all.
// This is particular a workaround for "go get" command which does not respect
// .netrc file.
func earlyResponseForGoGetMeta(ctx *Context) {
func EarlyResponseForGoGetMeta(ctx *Context) {
username := ctx.Params(":username")
reponame := ctx.Params(":reponame")
ctx.PlainText(200, []byte(com.Expand(`<meta name="go-import" content="{GoGetImport} git {CloneLink}">`,
map[string]string{
"GoGetImport": composeGoGetImport(ctx.Params(":username"), strings.TrimSuffix(ctx.Params(":reponame"), ".git")),
"CloneLink": models.ComposeHTTPSCloneURL(ctx.Params(":username"), ctx.Params(":reponame")),
"GoGetImport": ComposeGoGetImport(username, strings.TrimSuffix(reponame, ".git")),
"CloneLink": models.ComposeHTTPSCloneURL(username, reponame),
})))
}
@ -172,6 +174,75 @@ func RedirectToRepo(ctx *Context, redirectRepoID int64) {
ctx.Redirect(redirectPath)
}
// RepoIDAssignment returns an macaron handler which assigns the repo to the context.
func RepoIDAssignment() macaron.Handler {
return func(ctx *Context) {
var (
err error
)
repoID := ctx.ParamsInt64(":repoid")
// Get repository.
repo, err := models.GetRepositoryByID(repoID)
if err != nil {
if models.IsErrRepoNotExist(err) {
ctx.Handle(404, "GetRepositoryByID", nil)
} else {
ctx.Handle(500, "GetRepositoryByID", err)
}
return
}
if err = repo.GetOwner(); err != nil {
ctx.Handle(500, "GetOwner", err)
return
}
// Admin has super access.
if ctx.IsSigned && ctx.User.IsAdmin {
ctx.Repo.AccessMode = models.AccessModeOwner
} else {
var userID int64
if ctx.User != nil {
userID = ctx.User.ID
}
mode, err := models.AccessLevel(userID, repo)
if err != nil {
ctx.Handle(500, "AccessLevel", err)
return
}
ctx.Repo.AccessMode = mode
}
// Check access.
if ctx.Repo.AccessMode == models.AccessModeNone {
if ctx.Query("go-get") == "1" {
EarlyResponseForGoGetMeta(ctx)
return
}
ctx.Handle(404, "no access right", err)
return
}
ctx.Data["HasAccess"] = true
if repo.IsMirror {
ctx.Repo.Mirror, err = models.GetMirrorByRepoID(repo.ID)
if err != nil {
ctx.Handle(500, "GetMirror", err)
return
}
ctx.Data["MirrorEnablePrune"] = ctx.Repo.Mirror.EnablePrune
ctx.Data["MirrorInterval"] = ctx.Repo.Mirror.Interval
ctx.Data["Mirror"] = ctx.Repo.Mirror
}
ctx.Repo.Repository = repo
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
ctx.Data["IsBareRepo"] = ctx.Repo.Repository.IsBare
}
}
// RepoAssignment returns a macaron to handle repository assignment
func RepoAssignment() macaron.Handler {
return func(ctx *Context) {
@ -191,7 +262,7 @@ func RepoAssignment() macaron.Handler {
if err != nil {
if models.IsErrUserNotExist(err) {
if ctx.Query("go-get") == "1" {
earlyResponseForGoGetMeta(ctx)
EarlyResponseForGoGetMeta(ctx)
return
}
ctx.Handle(404, "GetUserByName", nil)
@ -213,7 +284,7 @@ func RepoAssignment() macaron.Handler {
RedirectToRepo(ctx, redirectRepoID)
} else if models.IsErrRepoRedirectNotExist(err) {
if ctx.Query("go-get") == "1" {
earlyResponseForGoGetMeta(ctx)
EarlyResponseForGoGetMeta(ctx)
return
}
ctx.Handle(404, "GetRepositoryByName", nil)
@ -246,7 +317,7 @@ func RepoAssignment() macaron.Handler {
// Check access.
if ctx.Repo.AccessMode == models.AccessModeNone {
if ctx.Query("go-get") == "1" {
earlyResponseForGoGetMeta(ctx)
EarlyResponseForGoGetMeta(ctx)
return
}
ctx.Handle(404, "no access right", err)
@ -288,6 +359,7 @@ func RepoAssignment() macaron.Handler {
count, err := models.GetReleaseCountByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{
IncludeDrafts: false,
IncludeTags: true,
})
if err != nil {
ctx.Handle(500, "GetReleaseCountByRepoID", err)
@ -373,7 +445,7 @@ func RepoAssignment() macaron.Handler {
ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest
if ctx.Query("go-get") == "1" {
ctx.Data["GoGetImport"] = composeGoGetImport(owner.Name, repo.Name)
ctx.Data["GoGetImport"] = ComposeGoGetImport(owner.Name, repo.Name)
prefix := setting.AppURL + path.Join(owner.Name, repo.Name, "src", ctx.Repo.BranchName)
ctx.Data["GoDocDirectory"] = prefix + "{/dir}"
ctx.Data["GoDocFile"] = prefix + "{/dir}/{file}#L{line}"

View File

@ -15,6 +15,7 @@ import (
"github.com/blevesearch/bleve/analysis/token/lowercase"
"github.com/blevesearch/bleve/analysis/token/unicodenorm"
"github.com/blevesearch/bleve/analysis/tokenizer/unicode"
"github.com/blevesearch/bleve/index/upsidedown"
)
// issueIndexer (thread-safe) index for searching issues
@ -39,27 +40,25 @@ const issueIndexerAnalyzer = "issueIndexer"
// InitIssueIndexer initialize issue indexer
func InitIssueIndexer(populateIndexer func() error) {
_, err := os.Stat(setting.Indexer.IssuePath)
if err != nil {
if os.IsNotExist(err) {
if err = createIssueIndexer(); err != nil {
log.Fatal(4, "CreateIssuesIndexer: %v", err)
}
if err = populateIndexer(); err != nil {
log.Fatal(4, "PopulateIssuesIndex: %v", err)
}
} else {
log.Fatal(4, "InitIssuesIndexer: %v", err)
}
} else {
if err != nil && !os.IsNotExist(err) {
log.Fatal(4, "InitIssueIndexer: %v", err)
} else if err == nil {
issueIndexer, err = bleve.Open(setting.Indexer.IssuePath)
if err != nil {
log.Error(4, "Unable to open issues indexer (%s)."+
" If the error is due to incompatible versions, try deleting the indexer files;"+
" gitea will recreate them with the appropriate version the next time it runs."+
" Deleting the indexer files will not result in loss of data.",
setting.Indexer.IssuePath)
log.Fatal(4, "InitIssuesIndexer, open index: %v", err)
if err == nil {
return
} else if err != upsidedown.IncompatibleVersion {
log.Fatal(4, "InitIssueIndexer, open index: %v", err)
}
log.Warn("Incompatible bleve version, deleting and recreating issue indexer")
if err = os.RemoveAll(setting.Indexer.IssuePath); err != nil {
log.Fatal(4, "InitIssueIndexer: remove index, %v", err)
}
}
if err = createIssueIndexer(); err != nil {
log.Fatal(4, "InitIssuesIndexer: create index, %v", err)
}
if err = populateIndexer(); err != nil {
log.Fatal(4, "InitIssueIndexer: populate index, %v", err)
}
}

View File

@ -70,10 +70,7 @@ func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error {
return errHashMismatch
}
if err := os.Rename(tmpPath, path); err != nil {
return err
}
return nil
return os.Rename(tmpPath, path)
}
// Exists returns true if the object exists in the content store.

View File

@ -110,10 +110,7 @@ func (w *FileLogWriter) StartLogger() error {
return err
}
w.mw.SetFd(fd)
if err = w.initFd(); err != nil {
return err
}
return nil
return w.initFd()
}
func (w *FileLogWriter) docheck(size int) {

View File

@ -31,11 +31,11 @@ type Message struct {
}
// NewMessageFrom creates new mail message object with custom From header.
func NewMessageFrom(to []string, from, subject, body string) *Message {
func NewMessageFrom(to []string, fromDisplayName, fromAddress, subject, body string) *Message {
log.Trace("NewMessageFrom (body):\n%s", body)
msg := gomail.NewMessage()
msg.SetHeader("From", from)
msg.SetAddressHeader("From", fromAddress, fromDisplayName)
msg.SetHeader("To", to...)
msg.SetHeader("Subject", subject)
msg.SetDateHeader("Date", time.Now())
@ -58,7 +58,7 @@ func NewMessageFrom(to []string, from, subject, body string) *Message {
// NewMessage creates new mail message object with default From header.
func NewMessage(to []string, subject, body string) *Message {
return NewMessageFrom(to, setting.MailService.From, subject, body)
return NewMessageFrom(to, setting.MailService.FromName, setting.MailService.FromEmail, subject, body)
}
type loginAuth struct {

View File

@ -10,8 +10,8 @@ import (
"strings"
"testing"
_ "code.gitea.io/gitea/modules/markdown"
. "code.gitea.io/gitea/modules/markup"
_ "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"

View File

@ -17,8 +17,8 @@ import (
// Renderer is a extended version of underlying render object.
type Renderer struct {
blackfriday.Renderer
urlPrefix string
isWikiMarkdown bool
URLPrefix string
IsWiki bool
}
// Link defines how formal links should be processed to produce corresponding HTML elements.
@ -26,10 +26,10 @@ func (r *Renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []
if len(link) > 0 && !markup.IsLink(link) {
if link[0] != '#' {
lnk := string(link)
if r.isWikiMarkdown {
if r.IsWiki {
lnk = markup.URLJoin("wiki", lnk)
}
mLink := markup.URLJoin(r.urlPrefix, lnk)
mLink := markup.URLJoin(r.URLPrefix, lnk)
link = []byte(mLink)
}
}
@ -95,8 +95,8 @@ var (
// Image defines how images should be processed to produce corresponding HTML elements.
func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
prefix := r.urlPrefix
if r.isWikiMarkdown {
prefix := r.URLPrefix
if r.IsWiki {
prefix = markup.URLJoin(prefix, "wiki", "src")
}
prefix = strings.Replace(prefix, "/src/", "/raw/", 1)
@ -129,9 +129,9 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
htmlFlags |= blackfriday.HTML_SKIP_STYLE
htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
renderer := &Renderer{
Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""),
urlPrefix: urlPrefix,
isWikiMarkdown: wikiMarkdown,
Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""),
URLPrefix: urlPrefix,
IsWiki: wikiMarkdown,
}
// set up the parser

View File

@ -5,13 +5,11 @@
package markdown_test
import (
"fmt"
"strconv"
"strings"
"testing"
. "code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/markup"
. "code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
@ -21,45 +19,6 @@ const AppURL = "http://localhost:3000/"
const Repo = "gogits/gogs"
const AppSubURL = AppURL + Repo + "/"
var numericMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser",
"repo": "someRepo",
"style": markup.IssueNameStyleNumeric,
}
var alphanumericMetas = map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}",
"user": "someUser",
"repo": "someRepo",
"style": markup.IssueNameStyleAlphanumeric,
}
// numericLink an HTML to a numeric-style issue
func numericIssueLink(baseURL string, index int) string {
return link(markup.URLJoin(baseURL, strconv.Itoa(index)), fmt.Sprintf("#%d", index))
}
// alphanumLink an HTML link to an alphanumeric-style issue
func alphanumIssueLink(baseURL string, name string) string {
return link(markup.URLJoin(baseURL, name), name)
}
// urlContentsLink an HTML link whose contents is the target URL
func urlContentsLink(href string) string {
return link(href, href)
}
// link an HTML link
func link(href, contents string) string {
return fmt.Sprintf("<a href=\"%s\">%s</a>", href, contents)
}
func testRenderIssueIndexPattern(t *testing.T, input, expected string, metas map[string]string) {
assert.Equal(t, expected,
string(markup.RenderIssueIndexPattern([]byte(input), AppSubURL, metas)))
}
func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL
setting.AppSubURL = AppSubURL

View File

@ -7,8 +7,8 @@ package markup_test
import (
"testing"
_ "code.gitea.io/gitea/modules/markdown"
. "code.gitea.io/gitea/modules/markup"
_ "code.gitea.io/gitea/modules/markup/markdown"
"github.com/stretchr/testify/assert"
)

View File

@ -0,0 +1,56 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package markup
import (
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"github.com/chaseadamsio/goorgeous"
"github.com/russross/blackfriday"
)
func init() {
markup.RegisterParser(Parser{})
}
// Parser implements markup.Parser for orgmode
type Parser struct {
}
// Name implements markup.Parser
func (Parser) Name() string {
return "orgmode"
}
// Extensions implements markup.Parser
func (Parser) Extensions() []string {
return []string{".org"}
}
// Render renders orgmode rawbytes to HTML
func Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
htmlFlags := blackfriday.HTML_USE_XHTML
htmlFlags |= blackfriday.HTML_SKIP_STYLE
htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
renderer := &markdown.Renderer{
Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""),
URLPrefix: urlPrefix,
IsWiki: isWiki,
}
result := goorgeous.Org(rawBytes, renderer)
return result
}
// RenderString reners orgmode string to HTML string
func RenderString(rawContent string, urlPrefix string, metas map[string]string, isWiki bool) string {
return string(Render([]byte(rawContent), urlPrefix, metas, isWiki))
}
// Render implements markup.Parser
func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
return Render(rawBytes, urlPrefix, metas, isWiki)
}

View File

@ -0,0 +1,54 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package markup
import (
"strings"
"testing"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
const AppURL = "http://localhost:3000/"
const Repo = "gogits/gogs"
const AppSubURL = AppURL + Repo + "/"
func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
test := func(input, expected string) {
buffer := RenderString(input, setting.AppSubURL, nil, false)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
googleRendered := `<p><a href="https://google.com/" title="https://google.com/">https://google.com/</a></p>`
test("[[https://google.com/]]", googleRendered)
lnk := markup.URLJoin(AppSubURL, "WikiPage")
test("[[WikiPage][WikiPage]]",
`<p><a href="`+lnk+`" title="WikiPage">WikiPage</a></p>`)
}
func TestRender_Images(t *testing.T) {
setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
test := func(input, expected string) {
buffer := RenderString(input, setting.AppSubURL, nil, false)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
}
url := "../../.images/src/02/train.jpg"
title := "Train"
result := markup.URLJoin(AppSubURL, url)
test(
"[[file:"+url+"]["+title+"]]",
`<p><a href="`+result+`"><img src="`+result+`" alt="`+title+`" title="`+title+`" /></a></p>`)
}

View File

@ -1287,6 +1287,7 @@ type Mailer struct {
QueueLength int
Name string
From string
FromName string
FromEmail string
SendAsPlainText bool
@ -1345,6 +1346,7 @@ func newMailService() {
if err != nil {
log.Fatal(4, "Invalid mailer.FROM (%s): %v", MailService.From, err)
}
MailService.FromName = parsed.Name
MailService.FromEmail = parsed.Address
log.Info("Mail Service Enabled")

View File

@ -277,7 +277,7 @@ func RenderCommitMessage(full bool, msg, urlPrefix string, metas map[string]stri
// Actioner describes an action
type Actioner interface {
GetOpType() int
GetOpType() models.ActionType
GetActUserName() string
GetRepoUserName() string
GetRepoName() string
@ -289,25 +289,24 @@ type Actioner interface {
GetIssueInfos() []string
}
// ActionIcon accepts a int that represents action operation type
// and returns a icon class name.
func ActionIcon(opType int) string {
// ActionIcon accepts an action operation type and returns an icon class name.
func ActionIcon(opType models.ActionType) string {
switch opType {
case 1, 8: // Create and transfer repository
case models.ActionCreateRepo, models.ActionTransferRepo:
return "repo"
case 5, 9: // Commit repository
case models.ActionCommitRepo, models.ActionPushTag, models.ActionDeleteTag, models.ActionDeleteBranch:
return "git-commit"
case 6: // Create issue
case models.ActionCreateIssue:
return "issue-opened"
case 7: // New pull request
case models.ActionCreatePullRequest:
return "git-pull-request"
case 10: // Comment issue
case models.ActionCommentIssue:
return "comment-discussion"
case 11: // Merge pull request
case models.ActionMergePullRequest:
return "git-merge"
case 12, 14: // Close issue or pull request
case models.ActionCloseIssue, models.ActionClosePullRequest:
return "issue-closed"
case 13, 15: // Reopen issue or pull request
case models.ActionReopenIssue, models.ActionReopenPullRequest:
return "issue-reopened"
default:
return "invalid type"

View File

@ -20,6 +20,7 @@ notifications = Notifications
create_new = Create...
user_profile_and_more = User profile and more
signed_in_as = Signed in as
enable_javascript = This website works better with JavaScript
username = Username
email = Email
@ -1472,6 +1473,8 @@ comment_issue = `commented on issue <a href="%s/issues/%s">%s#%[2]s</a>`
merge_pull_request = `merged pull request <a href="%s/pulls/%s">%s#%[2]s</a>`
transfer_repo = transferred repository <code>%s</code> to <a href="%s">%s</a>
push_tag = pushed tag <a href="%s/src/%s">%[2]s</a> to <a href="%[1]s">%[3]s</a>
delete_tag = deleted tag %[2]s from <a href="%[1]s">%[3]s</a>
delete_branch = deleted branch %[2]s from <a href="%[1]s">%[3]s</a>
compare_commits = Compare %d commits
[tool]

6
package.json Normal file
View File

@ -0,0 +1,6 @@
{
"license": "MIT",
"dependencies": {
"less": "^2.7.2"
}
}

File diff suppressed because one or more lines are too long

View File

@ -1579,6 +1579,7 @@ $(document).ready(function () {
initCodeView();
initVueApp();
initTeamSettings();
initCtrlEnterSubmit();
// Repo clone url.
if ($('#repo-clone-url').length > 0) {
@ -1786,6 +1787,14 @@ function initVueComponents(){
})
}
function initCtrlEnterSubmit() {
$(".js-quick-submit").keydown(function(e) {
if (((e.ctrlKey && !e.altKey) || e.metaKey) && (e.keyCode == 13 || e.keyCode == 10)) {
$(this).closest("form").submit();
}
});
}
function initVueApp() {
var el = document.getElementById('app');
if (!el) {

View File

@ -8,8 +8,8 @@ import (
api "code.gitea.io/sdk/gitea"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
)

View File

@ -5,8 +5,6 @@
package repo
import (
"strings"
api "code.gitea.io/sdk/gitea"
"code.gitea.io/gitea/models"
@ -36,6 +34,7 @@ func GetRelease(ctx *context.APIContext) {
func ListReleases(ctx *context.APIContext) {
releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{
IncludeDrafts: ctx.Repo.AccessMode >= models.AccessModeWrite,
IncludeTags: false,
}, 1, 2147483647)
if err != nil {
ctx.Error(500, "GetReleasesByRepoID", err)
@ -62,43 +61,49 @@ func CreateRelease(ctx *context.APIContext, form api.CreateReleaseOption) {
ctx.Status(404)
return
}
tag, err := ctx.Repo.GitRepo.GetTag(form.TagName)
rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
if err != nil {
ctx.Error(500, "GetTag", err)
return
}
commit, err := tag.Commit()
if err != nil {
ctx.Error(500, "Commit", err)
return
}
commitsCount, err := commit.CommitsCount()
if err != nil {
ctx.Error(500, "CommitsCount", err)
return
}
rel := &models.Release{
RepoID: ctx.Repo.Repository.ID,
PublisherID: ctx.User.ID,
Publisher: ctx.User,
TagName: form.TagName,
LowerTagName: strings.ToLower(form.TagName),
Target: form.Target,
Title: form.Title,
Sha1: commit.ID.String(),
NumCommits: commitsCount,
Note: form.Note,
IsDraft: form.IsDraft,
IsPrerelease: form.IsPrerelease,
CreatedUnix: commit.Author.When.Unix(),
}
if err := models.CreateRelease(ctx.Repo.GitRepo, rel, nil); err != nil {
if models.IsErrReleaseAlreadyExist(err) {
ctx.Status(409)
} else {
ctx.Error(500, "CreateRelease", err)
if !models.IsErrReleaseNotExist(err) {
ctx.Handle(500, "GetRelease", err)
return
}
rel = &models.Release{
RepoID: ctx.Repo.Repository.ID,
PublisherID: ctx.User.ID,
Publisher: ctx.User,
TagName: form.TagName,
Target: form.Target,
Title: form.Title,
Note: form.Note,
IsDraft: form.IsDraft,
IsPrerelease: form.IsPrerelease,
IsTag: false,
}
if err := models.CreateRelease(ctx.Repo.GitRepo, rel, nil); err != nil {
if models.IsErrReleaseAlreadyExist(err) {
ctx.Status(409)
} else {
ctx.Error(500, "CreateRelease", err)
}
return
}
} else {
if !rel.IsTag {
ctx.Status(409)
return
}
rel.Title = form.Title
rel.Note = form.Note
rel.IsDraft = form.IsDraft
rel.IsPrerelease = form.IsPrerelease
rel.PublisherID = ctx.User.ID
rel.IsTag = false
if err = models.UpdateRelease(ctx.Repo.GitRepo, rel, nil); err != nil {
ctx.Handle(500, "UpdateRelease", err)
return
}
return
}
ctx.JSON(201, rel.APIFormat())
}
@ -111,11 +116,12 @@ func EditRelease(ctx *context.APIContext, form api.EditReleaseOption) {
}
id := ctx.ParamsInt64(":id")
rel, err := models.GetReleaseByID(id)
if err != nil {
if err != nil && !models.IsErrReleaseNotExist(err) {
ctx.Error(500, "GetReleaseByID", err)
return
}
if rel.RepoID != ctx.Repo.Repository.ID {
if err != nil && models.IsErrReleaseNotExist(err) ||
rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID {
ctx.Status(404)
return
}
@ -162,12 +168,13 @@ func DeleteRelease(ctx *context.APIContext) {
return
}
id := ctx.ParamsInt64(":id")
release, err := models.GetReleaseByID(id)
if err != nil {
rel, err := models.GetReleaseByID(id)
if err != nil && !models.IsErrReleaseNotExist(err) {
ctx.Error(500, "GetReleaseByID", err)
return
}
if release.RepoID != ctx.Repo.Repository.ID {
if err != nil && models.IsErrReleaseNotExist(err) ||
rel.IsTag || rel.RepoID != ctx.Repo.Repository.ID {
ctx.Status(404)
return
}

View File

@ -86,27 +86,27 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
repos []*models.Repository
count int64
err error
orderBy string
orderBy models.SearchOrderBy
)
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "oldest":
orderBy = "created_unix ASC"
orderBy = models.SearchOrderByOldest
case "recentupdate":
orderBy = "updated_unix DESC"
orderBy = models.SearchOrderByRecentUpdated
case "leastupdate":
orderBy = "updated_unix ASC"
orderBy = models.SearchOrderByLeastUpdated
case "reversealphabetically":
orderBy = "name DESC"
orderBy = models.SearchOrderByAlphabeticallyReverse
case "alphabetically":
orderBy = "name ASC"
orderBy = models.SearchOrderByAlphabetically
case "reversesize":
orderBy = "size DESC"
orderBy = models.SearchOrderBySizeReverse
case "size":
orderBy = "size ASC"
orderBy = models.SearchOrderBySize
default:
orderBy = "created_unix DESC"
orderBy = models.SearchOrderByNewest
}
keyword := strings.Trim(ctx.Query("q"), " ")

View File

@ -22,35 +22,15 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/com"
)
func composeGoGetImport(owner, repo, sub string) string {
return path.Join(setting.Domain, setting.AppSubURL, owner, repo, sub)
}
// earlyResponseForGoGetMeta responses appropriate go-get meta with status 200
// if user does not have actual access to the requested repository,
// or the owner or repository does not exist at all.
// This is particular a workaround for "go get" command which does not respect
// .netrc file.
func earlyResponseForGoGetMeta(ctx *context.Context, username, reponame, subpath string) {
ctx.PlainText(200, []byte(com.Expand(`<meta name="go-import" content="{GoGetImport} git {CloneLink}">`,
map[string]string{
"GoGetImport": composeGoGetImport(username, reponame, subpath),
"CloneLink": models.ComposeHTTPSCloneURL(username, reponame),
})))
}
// HTTP implmentation git smart HTTP protocol
func HTTP(ctx *context.Context) {
username := ctx.Params(":username")
reponame := strings.TrimSuffix(ctx.Params(":reponame"), ".git")
subpath := ctx.Params("*")
if ctx.Query("go-get") == "1" {
earlyResponseForGoGetMeta(ctx, username, reponame, subpath)
context.EarlyResponseForGoGetMeta(ctx)
return
}

View File

@ -24,7 +24,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/indexer"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"

View File

@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"github.com/Unknwon/paginater"
@ -67,6 +67,7 @@ func Releases(ctx *context.Context) {
opts := models.FindReleasesOptions{
IncludeDrafts: ctx.Repo.IsWriter(),
IncludeTags: true,
}
releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts, page, limit)
@ -145,57 +146,61 @@ func NewReleasePost(ctx *context.Context, form auth.NewReleaseForm) {
return
}
var tagCreatedUnix int64
tag, err := ctx.Repo.GitRepo.GetTag(form.TagName)
if err == nil {
commit, err := tag.Commit()
if err == nil {
tagCreatedUnix = commit.Author.When.Unix()
}
}
commit, err := ctx.Repo.GitRepo.GetBranchCommit(form.Target)
if err != nil {
ctx.Handle(500, "GetBranchCommit", err)
return
}
commitsCount, err := commit.CommitsCount()
if err != nil {
ctx.Handle(500, "CommitsCount", err)
return
}
rel := &models.Release{
RepoID: ctx.Repo.Repository.ID,
PublisherID: ctx.User.ID,
Title: form.Title,
TagName: form.TagName,
Target: form.Target,
Sha1: commit.ID.String(),
NumCommits: commitsCount,
Note: form.Content,
IsDraft: len(form.Draft) > 0,
IsPrerelease: form.Prerelease,
CreatedUnix: tagCreatedUnix,
}
var attachmentUUIDs []string
if setting.AttachmentEnabled {
attachmentUUIDs = form.Files
}
if err = models.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs); err != nil {
ctx.Data["Err_TagName"] = true
switch {
case models.IsErrReleaseAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
case models.IsErrInvalidTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
default:
ctx.Handle(500, "CreateRelease", err)
rel, err := models.GetRelease(ctx.Repo.Repository.ID, form.TagName)
if err != nil {
if !models.IsErrReleaseNotExist(err) {
ctx.Handle(500, "GetRelease", err)
return
}
rel := &models.Release{
RepoID: ctx.Repo.Repository.ID,
PublisherID: ctx.User.ID,
Title: form.Title,
TagName: form.TagName,
Target: form.Target,
Note: form.Content,
IsDraft: len(form.Draft) > 0,
IsPrerelease: form.Prerelease,
IsTag: false,
}
if err = models.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs); err != nil {
ctx.Data["Err_TagName"] = true
switch {
case models.IsErrReleaseAlreadyExist(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
case models.IsErrInvalidTagName(err):
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
default:
ctx.Handle(500, "CreateRelease", err)
}
return
}
} else {
if !rel.IsTag {
ctx.Data["Err_TagName"] = true
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
return
}
rel.Title = form.Title
rel.Note = form.Content
rel.IsDraft = len(form.Draft) > 0
rel.IsPrerelease = form.Prerelease
rel.PublisherID = ctx.User.ID
rel.IsTag = false
if err = models.UpdateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs); err != nil {
ctx.Data["Err_TagName"] = true
ctx.Handle(500, "UpdateRelease", err)
return
}
return
}
log.Trace("Release created: %s/%s:%s", ctx.User.LowerName, ctx.Repo.Repository.Name, form.TagName)
@ -246,6 +251,10 @@ func EditReleasePost(ctx *context.Context, form auth.EditReleaseForm) {
}
return
}
if rel.IsTag {
ctx.Handle(404, "GetRelease", err)
return
}
ctx.Data["tag_name"] = rel.TagName
ctx.Data["tag_target"] = rel.Target
ctx.Data["title"] = rel.Title
@ -275,8 +284,7 @@ func EditReleasePost(ctx *context.Context, form auth.EditReleaseForm) {
// DeleteRelease delete a release
func DeleteRelease(ctx *context.Context) {
delTag := ctx.QueryBool("delTag")
if err := models.DeleteReleaseByID(ctx.QueryInt64("id"), ctx.User, delTag); err != nil {
if err := models.DeleteReleaseByID(ctx.QueryInt64("id"), ctx.User, true); err != nil {
ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
} else {
ctx.Flash.Success(ctx.Tr("repo.release.deletion_success"))

View File

@ -47,7 +47,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
ctx.Handle(500, "ListEntries", err)
return
}
entries.Sort()
entries.CustomSort(base.NaturalSortLess)
ctx.Data["Files"], err = entries.GetCommitsInfo(ctx.Repo.Commit, ctx.Repo.TreePath)
if err != nil {
@ -95,11 +95,11 @@ func renderDirectory(ctx *context.Context, treeLink string) {
buf = append(buf, d...)
newbuf := markup.Render(readmeFile.Name(), buf, treeLink, ctx.Repo.Repository.ComposeMetas())
if newbuf != nil {
ctx.Data["IsMarkdown"] = true
ctx.Data["IsMarkup"] = true
} else {
// FIXME This is the only way to show non-markdown files
// instead of a broken "View Raw" link
ctx.Data["IsMarkdown"] = true
ctx.Data["IsMarkup"] = false
newbuf = bytes.Replace(buf, []byte("\n"), []byte(`<br>`), -1)
}
ctx.Data["FileContent"] = string(newbuf)
@ -197,13 +197,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
tp := markup.Type(blob.Name())
isSupportedMarkup := tp != ""
// FIXME: currently set IsMarkdown for compatible
ctx.Data["IsMarkdown"] = isSupportedMarkup
readmeExist := isSupportedMarkup || markup.IsReadmeFile(blob.Name())
ctx.Data["IsMarkup"] = isSupportedMarkup
readmeExist := markup.IsReadmeFile(blob.Name())
ctx.Data["ReadmeExist"] = readmeExist
if readmeExist && isSupportedMarkup {
if isSupportedMarkup {
ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeMetas()))
} else if readmeExist {
ctx.Data["FileContent"] = string(bytes.Replace(buf, []byte("\n"), []byte(`<br>`), -1))
} else {
// Building code view blocks with line number on server side.
var fileContent string

View File

@ -18,8 +18,8 @@ import (
"code.gitea.io/gitea/modules/auth"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/markdown"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
)
const (

View File

@ -419,8 +419,10 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/create", bindIgnErr(auth.CreateRepoForm{}), repo.CreatePost)
m.Get("/migrate", repo.Migrate)
m.Post("/migrate", bindIgnErr(auth.MigrateRepoForm{}), repo.MigratePost)
m.Combo("/fork/:repoid").Get(repo.Fork).
Post(bindIgnErr(auth.CreateRepoForm{}), repo.ForkPost)
m.Group("/fork", func() {
m.Combo("/:repoid").Get(repo.Fork).
Post(bindIgnErr(auth.CreateRepoForm{}), repo.ForkPost)
}, context.RepoIDAssignment(), context.UnitTypes(), context.LoadRepoUnits(), context.CheckUnit(models.UnitTypeCode))
}, reqSignIn)
m.Group("/:username/:reponame", func() {

View File

@ -107,26 +107,26 @@ func Profile(ctx *context.Context) {
var (
repos []*models.Repository
count int64
orderBy string
orderBy models.SearchOrderBy
)
ctx.Data["SortType"] = ctx.Query("sort")
switch ctx.Query("sort") {
case "newest":
orderBy = "created_unix DESC"
orderBy = models.SearchOrderByNewest
case "oldest":
orderBy = "created_unix ASC"
orderBy = models.SearchOrderByOldest
case "recentupdate":
orderBy = "updated_unix DESC"
orderBy = models.SearchOrderByRecentUpdated
case "leastupdate":
orderBy = "updated_unix ASC"
orderBy = models.SearchOrderByLeastUpdated
case "reversealphabetically":
orderBy = "name DESC"
orderBy = models.SearchOrderByAlphabeticallyReverse
case "alphabetically":
orderBy = "name ASC"
orderBy = models.SearchOrderByAlphabetically
default:
ctx.Data["SortType"] = "recentupdate"
orderBy = "updated_unix DESC"
orderBy = models.SearchOrderByNewest
}
// set default sort value if sort is empty.
@ -149,7 +149,7 @@ func Profile(ctx *context.Context) {
case "stars":
ctx.Data["PageIsProfileStarList"] = true
if len(keyword) == 0 {
repos, err = ctxUser.GetStarredRepos(showPrivate, page, setting.UI.User.RepoPagingNum, orderBy)
repos, err = ctxUser.GetStarredRepos(showPrivate, page, setting.UI.User.RepoPagingNum, orderBy.String())
if err != nil {
ctx.Handle(500, "GetStarredRepos", err)
return
@ -182,7 +182,7 @@ func Profile(ctx *context.Context) {
default:
if len(keyword) == 0 {
var total int
repos, err = models.GetUserRepositories(ctxUser.ID, showPrivate, page, setting.UI.User.RepoPagingNum, orderBy)
repos, err = models.GetUserRepositories(ctxUser.ID, showPrivate, page, setting.UI.User.RepoPagingNum, orderBy.String())
if err != nil {
ctx.Handle(500, "GetRepositories", err)
return

View File

@ -76,6 +76,12 @@
<!-- Stylesheet -->
<link rel="stylesheet" href="{{AppSubUrl}}/vendor/plugins/semantic/semantic.min.css">
<link rel="stylesheet" href="{{AppSubUrl}}/css/index.css?v={{MD5 AppVer}}">
<noscript>
<style>
.dropdown:hover > .menu { display: block; }
.ui.secondary.menu .dropdown.item > .menu { margin-top: 0; }
</style>
</noscript>
{{if .RequireHighlightJS}}
<link rel="stylesheet" href="{{AppSubUrl}}/vendor/plugins/highlight/github.css">
@ -118,7 +124,7 @@
</head>
<body>
<div class="full height">
<noscript>Please enable JavaScript in your browser!</noscript>
<noscript>{{.i18n.Tr "enable_javascript"}}</noscript>
{{if not .PageIsInstall}}
<div class="following bar light">
@ -204,7 +210,7 @@
<i class="octicon octicon-settings"></i>
{{.i18n.Tr "your_settings"}}<!-- Your settings -->
</a>
<a class="item" target="_blank" rel="noopener" href="https://docs.gitea.io" rel="noreferrer">
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io">
<i class="octicon octicon-question"></i>
{{.i18n.Tr "help"}}<!-- Help -->
</a>
@ -228,7 +234,7 @@
{{else}}
<a class="item" target="_blank" rel="noopener" href="https://docs.gitea.io" rel="noreferrer">{{.i18n.Tr "help"}}</a>
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io">{{.i18n.Tr "help"}}</a>
<div class="right menu">
{{if .ShowRegistrationButton}}
<a class="item{{if .PageIsSignUp}} active{{end}}" href="{{AppSubUrl}}/user/sign_up">

View File

@ -4,7 +4,7 @@
<a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}">{{.i18n.Tr "repo.release.preview"}}</a>
</div>
<div class="ui bottom attached active tab segment" data-tab="write">
<textarea id="content" class="edit_area" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.Repo.RepoLink}}">
<textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.Repo.RepoLink}}">
{{if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end}}</textarea>
</div>
<div class="ui bottom attached tab segment markdown" data-tab="preview">

View File

@ -17,7 +17,9 @@
{{range .Releases}}
<li class="ui grid">
<div class="ui four wide column meta">
{{if .PublisherID}}
{{if .IsTag}}
{{if .Created}}<span class="time">{{TimeSince .Created $.Lang}}</span>{{end}}
{{else}}
{{if .IsDraft}}
<span class="ui yellow label">{{$.i18n.Tr "repo.release.draft"}}</span>
{{else if .IsPrerelease}}
@ -28,13 +30,22 @@
<span class="tag text blue">
<a href="{{$.RepoLink}}/src/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
</span>
<span class="commit">
<a href="{{$.RepoLink}}/src/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
</span>
{{end}}
<span class="commit">
<a href="{{$.RepoLink}}/src/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
</span>
</div>
<div class="ui twelve wide column detail">
{{if .PublisherID}}
{{if .IsTag}}
<h4>
<a href="{{$.RepoLink}}/src/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
</h4>
<div class="download">
<a href="{{$.RepoLink}}/src/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
<a href="{{$.RepoLink}}/archive/{{.TagName}}.zip" rel="nofollow"><i class="octicon octicon-file-zip"></i> ZIP</a>
<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> TAR.GZ</a>
</div>
{{else}}
<h3>
<a href="{{$.RepoLink}}/src/{{.TagName}}">{{.Title}}</a>
{{if $.IsRepositoryWriter}}<small>(<a href="{{$.RepoLink}}/releases/edit/{{.TagName}}" rel="nofollow">{{$.i18n.Tr "repo.release.edit"}}</a>)</small>{{end}}
@ -70,14 +81,6 @@
{{end}}
</ul>
</div>
{{else}}
<h4>
<a href="{{$.RepoLink}}/src/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
</h4>
<div class="download">
<a href="{{$.RepoLink}}/archive/{{.TagName}}.zip" rel="nofollow"><i class="octicon octicon-file-zip"></i> ZIP</a>
<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> TAR.GZ</a>
</div>
{{end}}
<span class="dot">&nbsp;</span>
</div>

View File

@ -36,8 +36,8 @@
{{end}}
</h4>
<div class="ui attached table segment">
<div class="file-view {{if .IsMarkdown}}markdown{{else if .IsTextFile}}code-view{{end}} has-emoji">
{{if .IsMarkdown}}
<div class="file-view {{if .IsMarkup}}markdown{{else if .IsTextFile}}code-view{{end}} has-emoji">
{{if .IsMarkup}}
{{if .FileContent}}{{.FileContent | Str2html}}{{end}}
{{else if not .IsTextFile}}
<div class="view-raw ui center">

View File

@ -43,6 +43,12 @@
{{else if eq .GetOpType 15}}
{{ $index := index .GetIssueInfos 0}}
{{$.i18n.Tr "action.reopen_pull_request" .GetRepoLink $index .ShortRepoPath | Str2html}}
{{else if eq .GetOpType 16}}
{{ $index := index .GetIssueInfos 0}}
{{$.i18n.Tr "action.delete_tag" .GetRepoLink .GetBranch .ShortRepoPath | Str2html}}
{{else if eq .GetOpType 17}}
{{ $index := index .GetIssueInfos 0}}
{{$.i18n.Tr "action.delete_branch" .GetRepoLink .GetBranch .ShortRepoPath | Str2html}}
{{end}}
</p>
{{if eq .GetOpType 5}}

View File

@ -116,35 +116,51 @@ func (te *TreeEntry) GetSubJumpablePathName() string {
// Entries a list of entry
type Entries []*TreeEntry
var sorter = []func(t1, t2 *TreeEntry) bool{
func(t1, t2 *TreeEntry) bool {
type customSortableEntries struct {
Comparer func(s1, s2 string) bool
Entries
}
var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
},
func(t1, t2 *TreeEntry) bool {
return t1.name < t2.name
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
return cmp(t1.name, t2.name)
},
}
func (tes Entries) Len() int { return len(tes) }
func (tes Entries) Swap(i, j int) { tes[i], tes[j] = tes[j], tes[i] }
func (tes Entries) Less(i, j int) bool {
t1, t2 := tes[i], tes[j]
func (ctes customSortableEntries) Len() int { return len(ctes.Entries) }
func (ctes customSortableEntries) Swap(i, j int) {
ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i]
}
func (ctes customSortableEntries) Less(i, j int) bool {
t1, t2 := ctes.Entries[i], ctes.Entries[j]
var k int
for k = 0; k < len(sorter)-1; k++ {
s := sorter[k]
switch {
case s(t1, t2):
case s(t1, t2, ctes.Comparer):
return true
case s(t2, t1):
case s(t2, t1, ctes.Comparer):
return false
}
}
return sorter[k](t1, t2)
return sorter[k](t1, t2, ctes.Comparer)
}
// Sort sort the list of entry
func (tes Entries) Sort() {
sort.Sort(tes)
sort.Sort(customSortableEntries{func(s1, s2 string) bool {
return s1 < s2
}, tes})
}
// CustomSort customizable string comparing sort entry list
func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) {
sort.Sort(customSortableEntries{cmp, tes})
}
type commitInfo struct {

21
vendor/github.com/chaseadamsio/goorgeous/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Chase Adams <realchaseadams@gmail.com>
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.

66
vendor/github.com/chaseadamsio/goorgeous/README.org generated vendored Normal file
View File

@ -0,0 +1,66 @@
#+TITLE: chaseadamsio/goorgeous
[[https://travis-ci.org/chaseadamsio/goorgeous.svg?branch=master]]
[[https://coveralls.io/repos/github/chaseadamsio/goorgeous/badge.svg?branch=master]]
/goorgeous is a Go Org to HTML Parser./
[[file:gopher_small.gif]]
*Pronounced: Go? Org? Yes!*
#+BEGIN_QUOTE
"Org mode is for keeping notes, maintaining TODO lists, planning projects, and authoring documents with a fast and effective plain-text system."
- [[orgmode.org]]
#+END_QUOTE
The purpose of this package is to come as close as possible as parsing an =*.org= document into HTML, the same way one might publish [[http://orgmode.org/worg/org-tutorials/org-publish-html-tutorial.html][with org-publish-html from Emacs]].
* Installation
#+BEGIN_SRC sh
go get -u github.com/chaseadamsio/goorgeous
#+END_SRC
* Usage
** Org Headers
To retrieve the headers from a =[]byte=, call =OrgHeaders= and it will return a =map[string]interface{}=:
#+BEGIN_SRC go
input := "#+title: goorgeous\n* Some Headline\n"
out := goorgeous.OrgHeaders(input)
#+END_SRC
#+BEGIN_SRC go
map[string]interface{}{
"title": "goorgeous"
}
#+END_SRC
** Org Content
After importing =github.com/chaseadamsio/goorgeous=, you can call =Org= with a =[]byte= and it will return an =html= version of the content as a =[]byte=
#+BEGIN_SRC go
input := "#+TITLE: goorgeous\n* Some Headline\n"
out := goorgeous.Org(input)
#+END_SRC
=out= will be:
#+BEGIN_SRC html
<h1>Some Headline</h1>/n
#+END_SRC
* Why?
First off, I've become an unapologetic user of Emacs & ever since finding =org-mode= I use it for anything having to do with writing content, organizing my life and keeping documentation of my days/weeks/months.
Although I like Emacs & =emacs-lisp=, I publish all of my html sites with [[https://gohugo.io][Hugo Static Site Generator]] and wanted to be able to write my content in =org-mode= in Emacs rather than markdown.
Hugo's implementation of templating and speed are unmatched, so the only way I knew for sure I could continue to use Hugo and write in =org-mode= seamlessly was to write a golang parser for org content and submit a PR for Hugo to use it.
* Acknowledgements
I leaned heavily on russross' [[https://github.com/russross/blackfriday][blackfriday markdown renderer]] as both an example of how to write a parser (with some updates to leverage the go we know today) and reusing the blackfriday HTML Renderer so I didn't have to write my own!

803
vendor/github.com/chaseadamsio/goorgeous/goorgeous.go generated vendored Normal file
View File

@ -0,0 +1,803 @@
package goorgeous
import (
"bufio"
"bytes"
"regexp"
"github.com/russross/blackfriday"
"github.com/shurcooL/sanitized_anchor_name"
)
type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int
type footnotes struct {
id string
def string
}
type parser struct {
r blackfriday.Renderer
inlineCallback [256]inlineParser
notes []footnotes
}
// NewParser returns a new parser with the inlineCallbacks required for org content
func NewParser(renderer blackfriday.Renderer) *parser {
p := new(parser)
p.r = renderer
p.inlineCallback['='] = generateVerbatim
p.inlineCallback['~'] = generateCode
p.inlineCallback['/'] = generateEmphasis
p.inlineCallback['_'] = generateUnderline
p.inlineCallback['*'] = generateBold
p.inlineCallback['+'] = generateStrikethrough
p.inlineCallback['['] = generateLinkOrImg
return p
}
// OrgCommon is the easiest way to parse a byte slice of org content and makes assumptions
// that the caller wants to use blackfriday's HTMLRenderer with XHTML
func OrgCommon(input []byte) []byte {
renderer := blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML, "", "")
return OrgOptions(input, renderer)
}
// Org is a convenience name for OrgOptions
func Org(input []byte, renderer blackfriday.Renderer) []byte {
return OrgOptions(input, renderer)
}
// OrgOptions takes an org content byte slice and a renderer to use
func OrgOptions(input []byte, renderer blackfriday.Renderer) []byte {
// in the case that we need to render something in isEmpty but there isn't a new line char
input = append(input, '\n')
var output bytes.Buffer
p := NewParser(renderer)
scanner := bufio.NewScanner(bytes.NewReader(input))
// used to capture code blocks
marker := ""
syntax := ""
listType := ""
inParagraph := false
inList := false
inTable := false
inFixedWidthArea := false
var tmpBlock bytes.Buffer
for scanner.Scan() {
data := scanner.Bytes()
if !isEmpty(data) && isComment(data) || IsKeyword(data) {
switch {
case inList:
if tmpBlock.Len() > 0 {
p.generateList(&output, tmpBlock.Bytes(), listType)
}
inList = false
listType = ""
tmpBlock.Reset()
case inTable:
if tmpBlock.Len() > 0 {
p.generateTable(&output, tmpBlock.Bytes())
}
inTable = false
tmpBlock.Reset()
case inParagraph:
if tmpBlock.Len() > 0 {
p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
}
inParagraph = false
tmpBlock.Reset()
case inFixedWidthArea:
if tmpBlock.Len() > 0 {
tmpBlock.WriteString("</pre>\n")
output.Write(tmpBlock.Bytes())
}
inFixedWidthArea = false
tmpBlock.Reset()
}
}
switch {
case isEmpty(data):
switch {
case inList:
if tmpBlock.Len() > 0 {
p.generateList(&output, tmpBlock.Bytes(), listType)
}
inList = false
listType = ""
tmpBlock.Reset()
case inTable:
if tmpBlock.Len() > 0 {
p.generateTable(&output, tmpBlock.Bytes())
}
inTable = false
tmpBlock.Reset()
case inParagraph:
if tmpBlock.Len() > 0 {
p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
}
inParagraph = false
tmpBlock.Reset()
case inFixedWidthArea:
if tmpBlock.Len() > 0 {
tmpBlock.WriteString("</pre>\n")
output.Write(tmpBlock.Bytes())
}
inFixedWidthArea = false
tmpBlock.Reset()
case marker != "":
tmpBlock.WriteByte('\n')
default:
continue
}
case isPropertyDrawer(data) || marker == "PROPERTIES":
if marker == "" {
marker = "PROPERTIES"
}
if bytes.Equal(data, []byte(":END:")) {
marker = ""
}
continue
case isBlock(data) || marker != "":
matches := reBlock.FindSubmatch(data)
if len(matches) > 0 {
if string(matches[1]) == "END" {
switch marker {
case "QUOTE":
var tmpBuf bytes.Buffer
p.inline(&tmpBuf, tmpBlock.Bytes())
p.r.BlockQuote(&output, tmpBuf.Bytes())
case "CENTER":
var tmpBuf bytes.Buffer
output.WriteString("<center>\n")
p.inline(&tmpBuf, tmpBlock.Bytes())
output.Write(tmpBuf.Bytes())
output.WriteString("</center>\n")
default:
tmpBlock.WriteByte('\n')
p.r.BlockCode(&output, tmpBlock.Bytes(), syntax)
}
marker = ""
tmpBlock.Reset()
continue
}
}
if marker != "" {
if marker != "SRC" && marker != "EXAMPLE" {
var tmpBuf bytes.Buffer
tmpBuf.Write([]byte("<p>\n"))
p.inline(&tmpBuf, data)
tmpBuf.WriteByte('\n')
tmpBuf.Write([]byte("</p>\n"))
tmpBlock.Write(tmpBuf.Bytes())
} else {
tmpBlock.WriteByte('\n')
tmpBlock.Write(data)
}
} else {
marker = string(matches[2])
syntax = string(matches[3])
}
case isFootnoteDef(data):
matches := reFootnoteDef.FindSubmatch(data)
for i := range p.notes {
if p.notes[i].id == string(matches[1]) {
p.notes[i].def = string(matches[2])
}
}
case isTable(data):
if inTable != true {
inTable = true
}
tmpBlock.Write(data)
tmpBlock.WriteByte('\n')
case IsKeyword(data):
continue
case isComment(data):
p.generateComment(&output, data)
case isHeadline(data):
p.generateHeadline(&output, data)
case isDefinitionList(data):
if inList != true {
listType = "dl"
inList = true
}
var work bytes.Buffer
flags := blackfriday.LIST_TYPE_DEFINITION
matches := reDefinitionList.FindSubmatch(data)
flags |= blackfriday.LIST_TYPE_TERM
p.inline(&work, matches[1])
p.r.ListItem(&tmpBlock, work.Bytes(), flags)
work.Reset()
flags &= ^blackfriday.LIST_TYPE_TERM
p.inline(&work, matches[2])
p.r.ListItem(&tmpBlock, work.Bytes(), flags)
case isUnorderedList(data):
if inList != true {
listType = "ul"
inList = true
}
matches := reUnorderedList.FindSubmatch(data)
var work bytes.Buffer
p.inline(&work, matches[2])
p.r.ListItem(&tmpBlock, work.Bytes(), 0)
case isOrderedList(data):
if inList != true {
listType = "ol"
inList = true
}
matches := reOrderedList.FindSubmatch(data)
var work bytes.Buffer
tmpBlock.WriteString("<li")
if len(matches[2]) > 0 {
tmpBlock.WriteString(" value=\"")
tmpBlock.Write(matches[2])
tmpBlock.WriteString("\"")
matches[3] = matches[3][1:]
}
p.inline(&work, matches[3])
tmpBlock.WriteString(">")
tmpBlock.Write(work.Bytes())
tmpBlock.WriteString("</li>\n")
case isHorizontalRule(data):
p.r.HRule(&output)
case isExampleLine(data):
if inParagraph == true {
if len(tmpBlock.Bytes()) > 0 {
p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
inParagraph = false
}
tmpBlock.Reset()
}
if inFixedWidthArea != true {
tmpBlock.WriteString("<pre class=\"example\">\n")
inFixedWidthArea = true
}
matches := reExampleLine.FindSubmatch(data)
tmpBlock.Write(matches[1])
tmpBlock.WriteString("\n")
break
default:
if inParagraph == false {
inParagraph = true
if inFixedWidthArea == true {
if tmpBlock.Len() > 0 {
tmpBlock.WriteString("</pre>")
output.Write(tmpBlock.Bytes())
}
inFixedWidthArea = false
tmpBlock.Reset()
}
}
tmpBlock.Write(data)
tmpBlock.WriteByte('\n')
}
}
if len(tmpBlock.Bytes()) > 0 {
if inParagraph == true {
p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
} else if inFixedWidthArea == true {
tmpBlock.WriteString("</pre>\n")
output.Write(tmpBlock.Bytes())
}
}
// Writing footnote def. list
if len(p.notes) > 0 {
flags := blackfriday.LIST_ITEM_BEGINNING_OF_LIST
p.r.Footnotes(&output, func() bool {
for i := range p.notes {
p.r.FootnoteItem(&output, []byte(p.notes[i].id), []byte(p.notes[i].def), flags)
}
return true
})
}
return output.Bytes()
}
// Org Syntax has been broken up into 4 distinct sections based on
// the org-syntax draft (http://orgmode.org/worg/dev/org-syntax.html):
// - Headlines
// - Greater Elements
// - Elements
// - Objects
// Headlines
func isHeadline(data []byte) bool {
if !charMatches(data[0], '*') {
return false
}
level := 0
for level < 6 && charMatches(data[level], '*') {
level++
}
return charMatches(data[level], ' ')
}
func (p *parser) generateHeadline(out *bytes.Buffer, data []byte) {
level := 1
status := ""
priority := ""
for level < 6 && data[level] == '*' {
level++
}
start := skipChar(data, level, ' ')
data = data[start:]
i := 0
// Check if has a status so it can be rendered as a separate span that can be hidden or
// modified with CSS classes
if hasStatus(data[i:4]) {
status = string(data[i:4])
i += 5 // one extra character for the next whitespace
}
// Check if the next byte is a priority marker
if data[i] == '[' && hasPriority(data[i+1]) {
priority = string(data[i+1])
i += 4 // for "[c]" + ' '
}
tags, tagsFound := findTags(data, i)
headlineID := sanitized_anchor_name.Create(string(data[i:]))
generate := func() bool {
dataEnd := len(data)
if tagsFound > 0 {
dataEnd = tagsFound
}
headline := bytes.TrimRight(data[i:dataEnd], " \t")
if status != "" {
out.WriteString("<span class=\"todo " + status + "\">" + status + "</span>")
out.WriteByte(' ')
}
if priority != "" {
out.WriteString("<span class=\"priority " + priority + "\">[" + priority + "]</span>")
out.WriteByte(' ')
}
p.inline(out, headline)
if tagsFound > 0 {
for _, tag := range tags {
out.WriteByte(' ')
out.WriteString("<span class=\"tags " + tag + "\">" + tag + "</span>")
out.WriteByte(' ')
}
}
return true
}
p.r.Header(out, generate, level, headlineID)
}
func hasStatus(data []byte) bool {
return bytes.Contains(data, []byte("TODO")) || bytes.Contains(data, []byte("DONE"))
}
func hasPriority(char byte) bool {
return (charMatches(char, 'A') || charMatches(char, 'B') || charMatches(char, 'C'))
}
func findTags(data []byte, start int) ([]string, int) {
tags := []string{}
tagOpener := 0
tagMarker := tagOpener
for tIdx := start; tIdx < len(data); tIdx++ {
if tagMarker > 0 && data[tIdx] == ':' {
tags = append(tags, string(data[tagMarker+1:tIdx]))
tagMarker = tIdx
}
if data[tIdx] == ':' && tagOpener == 0 && data[tIdx-1] == ' ' {
tagMarker = tIdx
tagOpener = tIdx
}
}
return tags, tagOpener
}
// Greater Elements
// ~~ Definition Lists
var reDefinitionList = regexp.MustCompile(`^\s*-\s+(.+?)\s+::\s+(.*)`)
func isDefinitionList(data []byte) bool {
return reDefinitionList.Match(data)
}
// ~~ Example lines
var reExampleLine = regexp.MustCompile(`^\s*:\s(\s*.*)|^\s*:$`)
func isExampleLine(data []byte) bool {
return reExampleLine.Match(data)
}
// ~~ Ordered Lists
var reOrderedList = regexp.MustCompile(`^(\s*)\d+\.\s+\[?@?(\d*)\]?(.+)`)
func isOrderedList(data []byte) bool {
return reOrderedList.Match(data)
}
// ~~ Unordered Lists
var reUnorderedList = regexp.MustCompile(`^(\s*)[-\+]\s+(.+)`)
func isUnorderedList(data []byte) bool {
return reUnorderedList.Match(data)
}
// ~~ Tables
var reTableHeaders = regexp.MustCompile(`^[|+-]*$`)
func isTable(data []byte) bool {
return charMatches(data[0], '|')
}
func (p *parser) generateTable(output *bytes.Buffer, data []byte) {
var table bytes.Buffer
rows := bytes.Split(bytes.Trim(data, "\n"), []byte("\n"))
hasTableHeaders := len(rows) > 1
if len(rows) > 1 {
hasTableHeaders = reTableHeaders.Match(rows[1])
}
tbodySet := false
for idx, row := range rows {
var rowBuff bytes.Buffer
if hasTableHeaders && idx == 0 {
table.WriteString("<thead>")
for _, cell := range bytes.Split(row[1:len(row)-1], []byte("|")) {
p.r.TableHeaderCell(&rowBuff, bytes.Trim(cell, " \t"), 0)
}
p.r.TableRow(&table, rowBuff.Bytes())
table.WriteString("</thead>\n")
} else if hasTableHeaders && idx == 1 {
continue
} else {
if !tbodySet {
table.WriteString("<tbody>")
tbodySet = true
}
if !reTableHeaders.Match(row) {
for _, cell := range bytes.Split(row[1:len(row)-1], []byte("|")) {
var cellBuff bytes.Buffer
p.inline(&cellBuff, bytes.Trim(cell, " \t"))
p.r.TableCell(&rowBuff, cellBuff.Bytes(), 0)
}
p.r.TableRow(&table, rowBuff.Bytes())
}
if tbodySet && idx == len(rows)-1 {
table.WriteString("</tbody>\n")
tbodySet = false
}
}
}
output.WriteString("\n<table>\n")
output.Write(table.Bytes())
output.WriteString("</table>\n")
}
// ~~ Property Drawers
func isPropertyDrawer(data []byte) bool {
return bytes.Equal(data, []byte(":PROPERTIES:"))
}
// ~~ Dynamic Blocks
var reBlock = regexp.MustCompile(`^#\+(BEGIN|END)_(\w+)\s*([0-9A-Za-z_\-]*)?`)
func isBlock(data []byte) bool {
return reBlock.Match(data)
}
// ~~ Footnotes
var reFootnoteDef = regexp.MustCompile(`^\[fn:([\w]+)\] +(.+)`)
func isFootnoteDef(data []byte) bool {
return reFootnoteDef.Match(data)
}
// Elements
// ~~ Keywords
func IsKeyword(data []byte) bool {
return len(data) > 2 && charMatches(data[0], '#') && charMatches(data[1], '+') && !charMatches(data[2], ' ')
}
// ~~ Comments
func isComment(data []byte) bool {
return charMatches(data[0], '#') && charMatches(data[1], ' ')
}
func (p *parser) generateComment(out *bytes.Buffer, data []byte) {
var work bytes.Buffer
work.WriteString("<!-- ")
work.Write(data[2:])
work.WriteString(" -->")
work.WriteByte('\n')
out.Write(work.Bytes())
}
// ~~ Horizontal Rules
var reHorizontalRule = regexp.MustCompile(`^\s*?-----\s?$`)
func isHorizontalRule(data []byte) bool {
return reHorizontalRule.Match(data)
}
// ~~ Paragraphs
func (p *parser) generateParagraph(out *bytes.Buffer, data []byte) {
generate := func() bool {
p.inline(out, bytes.Trim(data, " "))
return true
}
p.r.Paragraph(out, generate)
}
func (p *parser) generateList(output *bytes.Buffer, data []byte, listType string) {
generateList := func() bool {
output.WriteByte('\n')
p.inline(output, bytes.Trim(data, " "))
return true
}
switch listType {
case "ul":
p.r.List(output, generateList, 0)
case "ol":
p.r.List(output, generateList, blackfriday.LIST_TYPE_ORDERED)
case "dl":
p.r.List(output, generateList, blackfriday.LIST_TYPE_DEFINITION)
}
}
// Objects
func (p *parser) inline(out *bytes.Buffer, data []byte) {
i, end := 0, 0
for i < len(data) {
for end < len(data) && p.inlineCallback[data[end]] == nil {
end++
}
p.r.Entity(out, data[i:end])
if end >= len(data) {
break
}
i = end
handler := p.inlineCallback[data[i]]
if consumed := handler(p, out, data, i); consumed > 0 {
i += consumed
end = i
continue
}
end = i + 1
}
}
func isAcceptablePreOpeningChar(dataIn, data []byte, offset int) bool {
if len(dataIn) == len(data) {
return true
}
char := dataIn[offset-1]
return charMatches(char, ' ') || isPreChar(char)
}
func isPreChar(char byte) bool {
return charMatches(char, '>') || charMatches(char, '(') || charMatches(char, '{') || charMatches(char, '[')
}
func isAcceptablePostClosingChar(char byte) bool {
return charMatches(char, ' ') || isTerminatingChar(char)
}
func isTerminatingChar(char byte) bool {
return charMatches(char, '.') || charMatches(char, ',') || charMatches(char, '?') || charMatches(char, '!') || charMatches(char, ')') || charMatches(char, '}') || charMatches(char, ']')
}
func findLastCharInInline(data []byte, char byte) int {
timesFound := 0
last := 0
// Start from character after the inline indicator
for i := 1; i < len(data); i++ {
if timesFound == 1 {
break
}
if data[i] == char {
if len(data) == i+1 || (len(data) > i+1 && isAcceptablePostClosingChar(data[i+1])) {
last = i
timesFound += 1
}
}
}
return last
}
func generator(p *parser, out *bytes.Buffer, dataIn []byte, offset int, char byte, doInline bool, renderer func(*bytes.Buffer, []byte)) int {
data := dataIn[offset:]
c := byte(char)
start := 1
i := start
if len(data) <= 1 {
return 0
}
lastCharInside := findLastCharInInline(data, c)
// Org mode spec says a non-whitespace character must immediately follow.
// if the current char is the marker, then there's no text between, not a candidate
if isSpace(data[i]) || lastCharInside == i || !isAcceptablePreOpeningChar(dataIn, data, offset) {
return 0
}
if lastCharInside > 0 {
var work bytes.Buffer
if doInline {
p.inline(&work, data[start:lastCharInside])
renderer(out, work.Bytes())
} else {
renderer(out, data[start:lastCharInside])
}
next := lastCharInside + 1
return next
}
return 0
}
// ~~ Text Markup
func generateVerbatim(p *parser, out *bytes.Buffer, data []byte, offset int) int {
return generator(p, out, data, offset, '=', false, p.r.CodeSpan)
}
func generateCode(p *parser, out *bytes.Buffer, data []byte, offset int) int {
return generator(p, out, data, offset, '~', false, p.r.CodeSpan)
}
func generateEmphasis(p *parser, out *bytes.Buffer, data []byte, offset int) int {
return generator(p, out, data, offset, '/', true, p.r.Emphasis)
}
func generateUnderline(p *parser, out *bytes.Buffer, data []byte, offset int) int {
underline := func(out *bytes.Buffer, text []byte) {
out.WriteString("<span style=\"text-decoration: underline;\">")
out.Write(text)
out.WriteString("</span>")
}
return generator(p, out, data, offset, '_', true, underline)
}
func generateBold(p *parser, out *bytes.Buffer, data []byte, offset int) int {
return generator(p, out, data, offset, '*', true, p.r.DoubleEmphasis)
}
func generateStrikethrough(p *parser, out *bytes.Buffer, data []byte, offset int) int {
return generator(p, out, data, offset, '+', true, p.r.StrikeThrough)
}
// ~~ Images and Links (inc. Footnote)
var reLinkOrImg = regexp.MustCompile(`\[\[(.+?)\]\[?(.*?)\]?\]`)
func generateLinkOrImg(p *parser, out *bytes.Buffer, data []byte, offset int) int {
data = data[offset+1:]
start := 1
i := start
var hyperlink []byte
isImage := false
isFootnote := false
closedLink := false
hasContent := false
if bytes.Equal(data[0:3], []byte("fn:")) {
isFootnote = true
} else if data[0] != '[' {
return 0
}
if bytes.Equal(data[1:6], []byte("file:")) {
isImage = true
}
for i < len(data) {
currChar := data[i]
switch {
case charMatches(currChar, ']') && closedLink == false:
if isImage {
hyperlink = data[start+5 : i]
} else if isFootnote {
refid := data[start+2 : i]
if bytes.Equal(refid, bytes.Trim(refid, " ")) {
p.notes = append(p.notes, footnotes{string(refid), "DEFINITION NOT FOUND"})
p.r.FootnoteRef(out, refid, len(p.notes))
return i + 2
} else {
return 0
}
} else if bytes.Equal(data[i-4:i], []byte(".org")) {
orgStart := start
if bytes.Equal(data[orgStart:orgStart+2], []byte("./")) {
orgStart = orgStart + 1
}
hyperlink = data[orgStart : i-4]
} else {
hyperlink = data[start:i]
}
closedLink = true
case charMatches(currChar, '['):
start = i + 1
hasContent = true
case charMatches(currChar, ']') && closedLink == true && hasContent == true && isImage == true:
p.r.Image(out, hyperlink, data[start:i], data[start:i])
return i + 3
case charMatches(currChar, ']') && closedLink == true && hasContent == true:
var tmpBuf bytes.Buffer
p.inline(&tmpBuf, data[start:i])
p.r.Link(out, hyperlink, tmpBuf.Bytes(), tmpBuf.Bytes())
return i + 3
case charMatches(currChar, ']') && closedLink == true && hasContent == false && isImage == true:
p.r.Image(out, hyperlink, hyperlink, hyperlink)
return i + 2
case charMatches(currChar, ']') && closedLink == true && hasContent == false:
p.r.Link(out, hyperlink, hyperlink, hyperlink)
return i + 2
}
i++
}
return 0
}
// Helpers
func skipChar(data []byte, start int, char byte) int {
i := start
for i < len(data) && charMatches(data[i], char) {
i++
}
return i
}
func isSpace(char byte) bool {
return charMatches(char, ' ')
}
func isEmpty(data []byte) bool {
if len(data) == 0 {
return true
}
for i := 0; i < len(data) && !charMatches(data[i], '\n'); i++ {
if !charMatches(data[i], ' ') && !charMatches(data[i], '\t') {
return false
}
}
return true
}
func charMatches(a byte, b byte) bool {
return a == b
}

BIN
vendor/github.com/chaseadamsio/goorgeous/gopher.gif generated vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

70
vendor/github.com/chaseadamsio/goorgeous/header.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
package goorgeous
import (
"bufio"
"bytes"
"regexp"
"strings"
)
// ExtractOrgHeaders finds and returns all of the headers
// from a bufio.Reader and returns them as their own byte slice
func ExtractOrgHeaders(r *bufio.Reader) (fm []byte, err error) {
var out bytes.Buffer
endOfHeaders := true
for endOfHeaders {
p, err := r.Peek(2)
if err != nil {
return nil, err
}
if !charMatches(p[0], '#') && !charMatches(p[1], '+') {
endOfHeaders = false
break
}
line, _, err := r.ReadLine()
if err != nil {
return nil, err
}
out.Write(line)
out.WriteByte('\n')
}
return out.Bytes(), nil
}
var reHeader = regexp.MustCompile(`^#\+(\w+?): (.*)`)
// OrgHeaders find all of the headers from a byte slice and returns
// them as a map of string interface
func OrgHeaders(input []byte) (map[string]interface{}, error) {
out := make(map[string]interface{})
scanner := bufio.NewScanner(bytes.NewReader(input))
for scanner.Scan() {
data := scanner.Bytes()
if !charMatches(data[0], '#') && !charMatches(data[1], '+') {
return out, nil
}
matches := reHeader.FindSubmatch(data)
if len(matches) < 3 {
continue
}
key := string(matches[1])
val := matches[2]
switch {
case strings.ToLower(key) == "tags" || strings.ToLower(key) == "categories" || strings.ToLower(key) == "aliases":
bTags := bytes.Split(val, []byte(" "))
tags := make([]string, len(bTags))
for idx, tag := range bTags {
tags[idx] = string(tag)
}
out[key] = tags
default:
out[key] = string(val)
}
}
return out, nil
}

12
vendor/vendor.json vendored
View File

@ -3,10 +3,10 @@
"ignore": "test appengine",
"package": [
{
"checksumSHA1": "fR5YDSoG7xYv2aLO23rne95gWps=",
"checksumSHA1": "9dxw/SGpdhNNm704gt6F02ItYtQ=",
"path": "code.gitea.io/git",
"revision": "479f87e5d189e7b8f1fd51dbcd25faa32b632cd2",
"revisionTime": "2017-08-03T00:53:29Z"
"revision": "d7487da878e40ee6c4fac7280b518c0ed0be702c",
"revisionTime": "2017-09-16T17:49:37Z"
},
{
"checksumSHA1": "Zgp5RqU+20L2p9TNl1rSsUIWEEE=",
@ -299,6 +299,12 @@
"revision": "fb1f79c6b65acda83063cbc69f6bba1522558bfc",
"revisionTime": "2016-01-17T19:21:50Z"
},
{
"checksumSHA1": "x1svIugw39oEZGU5/HMUHzgRUZM=",
"path": "github.com/chaseadamsio/goorgeous",
"revision": "098da33fde5f9220736531b3cb26a2dec86a8367",
"revisionTime": "2017-09-01T13:22:37Z"
},
{
"checksumSHA1": "agNqSytP0indDCoGizlMyC1L/m4=",
"path": "github.com/coreos/etcd/error",