KDOC 55: giteaのコードを読んだメモ

DONE プロジェクトステータス

プロジェクトは終了である。

概要

go-gitea/giteaを読んだメモ。バックエンドはGoで書かれている。

memo

コンパイル

コンパイルしている部分。タスク名が動的になっていてわかりにくい。

$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@

routerを起動している部分

webRoutes := routers.NormalRoutes()

DB設定部分

setting packageにまとめられている。パッケージで変数化されている。

// Database holds the database settings
Database = struct {

変数はグローバル変数にしつつ↑、セットはパッケージ内の関数でやる。

func loadDBSetting(rootCfg ConfigProvider) {
	sec := rootCfg.Section("database")
	Database.Type = DatabaseType(sec.Key("DB_TYPE").String())

	Database.Host = sec.Key("HOST").String()
	Database.Name = sec.Key("NAME").String()
	Database.User = sec.Key("USER").String()

DB初期化部分

routerでのDBの初期化は↓でやっている。

// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
func InitEngine(ctx context.Context) error {

InitEngineの中でデータベースを初期化する。

xormEngine, err := newXORMEngine()

InitEngineの中でSetDefaultEngine()して初期化したデータベースをDefaultContextに設定する。

// SetDefaultEngine sets the default engine for db
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
	x = eng
	DefaultContext = &Context{
		Context: ctx,
		e:       x,
	}
}

セットできているかの確認文。

mustInitCtx(ctx, common.InitDBEngine)

変数default contextに設定する

設定している部分。

// SetDefaultEngine sets the default engine for db
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
	x = eng
	DefaultContext = &Context{
		Context: ctx,
		e:       x,
	}
}

テストとかではdefault contextを使って、dbを簡単に起動している。そうでない部分では、起動時に明示して上書きしているようだ。

APIContextをセットしている部分

DBのデータなどはcontext.APIContextで伝達している。APIContextにはBaseが埋め込まれている。

// APIContext is a specific context for API service
type APIContext struct {
	*Base

	Cache cache.Cache

	Doer        *user_model.User // current signed-in user
	IsSigned    bool
	IsBasicAuth bool

	ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer

	Repo    *Repository
	Org     *APIOrganization
	Package *Package
}

Baseにはcontext.Contextが埋め込まれている。

type Base struct {
	originCtx     context.Context
	contextValues []contextValuePair

	Resp ResponseWriter
	Req  *http.Request

	// Data is prepared by ContextDataStore middleware, this field only refers to the pre-created/prepared ContextData.
	// Although it's mainly used for MVC templates, sometimes it's also used to pass data between middlewares/handler
	Data middleware.ContextData

	// Locale is mainly for Web context, although the API context also uses it in some cases: message response, form validation
	Locale translation.Locale
}
// APIContexter returns apicontext as middleware
func APIContexter() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			base, baseCleanUp := NewBaseContext(w, req)
			ctx := &APIContext{
				Base:  base,
				Cache: mc.GetCache(),
				Repo:  &Repository{PullRequest: &PullRequest{}},
				Org:   &APIOrganization{},
			}

contextからdbインスタンスを取り出す

has, err := db.GetEngine(ctx).ID(task.RepoID).Get(&repo)

DBコンテキストは異なる

DBのためだけにありそう。defaultContextが入るのはこれ。contextを構造体に埋め込むのはよくないとされている。

// Context represents a db context
type Context struct {
	context.Context
	e           Engine
	transaction bool
}
// DefaultContext is the default context to run xorm queries in
// will be overwritten by Init with HammerContext
var DefaultContext context.Context

context.Contextはインターフェースなので、DefaultContextにはdb.Contextが入る。

このDBコンテキストは、Engine()を実装しているのでEnginedインターフェースを満たす。

// Engined structs provide an Engine
type Engined interface {
	Engine() Engine
}

// GetEngine will get a db Engine from this context or return an Engine restricted to this context
func GetEngine(ctx context.Context) Engine {
	if e := getEngine(ctx); e != nil {
		return e
	}
	return x.Context(ctx)
}

// getEngine will get a db Engine from this context or return nil
func getEngine(ctx context.Context) Engine {
	if engined, ok := ctx.(Engined); ok {
		return engined.Engine()
	}
	enginedInterface := ctx.Value(enginedContextKey)
	if enginedInterface != nil {
		return enginedInterface.(Engined).Engine()
	}
	return nil
}
// Engine returns db engine
func (ctx *Context) Engine() Engine {
	return ctx.e
}

DBオブジェクト宣言

xormのオブジェクト作成はxorm.NewEngine()で行う。

engine, err = xorm.NewEngine("postgresschema", connStr)

テストで、どうやってDefaultContextをセットしているか

テストでたくさん使っている。便利だ。これはいつセットされているか。

最初にunittest.PrepareTestDatabase()する。この中でDefaultContextに対してクエリが実行されている。そのあとDefaultContextを直に取得して実行する。

assert.NoError(t, unittest.PrepareTestDatabase())
token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3})
expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0)

dbはcontextのvalueで持つべきじゃないらしい。リクエストスコープでないから。ダメじゃんと思ったけれども、contextの内部で持ってるわけではなく構造体で持ってるからいいのか。

APIコンテキストはどうやって生成しているか

// APIContexter returns apicontext as middleware
func APIContexter() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			base, baseCleanUp := NewBaseContext(w, req)
			ctx := &APIContext{
				Base:  base,
				Cache: mc.GetCache(),
				Repo:  &Repository{PullRequest: &PullRequest{}},
				Org:   &APIOrganization{},
			}
			defer baseCleanUp()

			ctx.Base.AppendContextValue(apiContextKey, ctx)
			ctx.Base.AppendContextValueFunc(git.RepositoryContextKey, func() any { return ctx.Repo.GitRepo })

			// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
			if ctx.Req.Method == "POST" && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
				if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
					ctx.InternalServerError(err)
					return
				}
			}

			httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 0, "no-transform")
			ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)

			next.ServeHTTP(ctx.Resp, ctx.Req)
		})
	}
}

ミドルウェアとして登録されている。

m.Use(context.APIContexter())

DB関係なくないか。どこでセットされているか。

トランザクションコンテキストは必要。なぜ。

マイグレーションコマンド

マイグレーションはタスクで行う。あるいは、Webの起動時にも関数が用意されている。

func runMigrate(ctx *cli.Context) error {

テスト時にどうやってDB初期化されているか

  • マイグレーション
  • DefaultContext初期化

が必要なはずだが見当たらない。

unittest.PrepareTestDatabase() を見るが、そのときにはすでにDefaultContextからDBが取得できている。

各test packageにあるtest_main.goにある、TestMain関数内でunittest.MainTest()している。MainTest内でDBを初期化してる。テスト時に必ず実行される。

func MainTest(m *testing.M, testOpts ...*TestOptions) {