diff --git a/.devops/testgitea环境.yml b/.devops/testgitea环境.yml index b2001d8..7ba216d 100644 --- a/.devops/testgitea环境.yml +++ b/.devops/testgitea环境.yml @@ -10,31 +10,68 @@ trigger: ruleset: - param-ref: branch operator: EQ - value: '"develop"' + value: '"feature"' ruleset-operator: AND workflow: - ref: start name: 开始 task: start - - ref: ssh_cmd_0 - name: ssh执行命令 - task: ssh_cmd@1.1.0 + - ref: git_clone_0 + name: git clone + on-failure: ignore + task: git_clone@1.2.6 input: - ssh_private_key: ((ssh.siyao)) - ssh_ip: '"123.59.135.93"' - ssh_port: '"51123"' - ssh_user: '"pdl"' - ssh_cmd: '"ssh root@10.9.69.134 -p30122 \" - cd /root - && - sh ./update.sh - && - echo done \""' + remote_url: '"https://gitlink.org.cn/Gitlink/gitea_hat.git"' + ref: '"refs/heads/feature"' + commit_id: '""' + depth: 1 needs: - start + - ref: golang_build_node_0 + name: golang_build_node + on-failure: ignore + task: yystopf/golang_build_node@0.0.2 + input: + workspace: git_clone_0.git_path + out_bin_name: '"gitea"' + goos: '"linux"' + goarch: '"amd64"' + needs: + - git_clone_0 + - ref: gitlink_scp_resource_0 + name: scp复制文件(支持跳板机) + on-failure: ignore + task: yystopf/gitlink_scp_resource@0.0.7 + input: + ssh_private_key: ((ssh.siyao)) + remote_host: '"10.9.69.134"' + remote_port: '"30122"' + remote_user: '"root"' + remote_file: '"/root/gitea"' + local_file: golang_build_node_0.bin_dir + gateway_host: '"123.59.135.93"' + gateway_port: '"51123"' + gateway_user: '"pdl"' + temp_file: '"/home/pdl/gitea"' + needs: + - golang_build_node_0 + - ref: gitlink_ssh_cmd_0 + name: ssh执行命令(支持跳板机) + task: yystopf/gitlink_ssh_cmd@0.0.6 + input: + ssh_private_key: ((ssh.siyao)) + remote_host: '"10.9.69.134"' + remote_port: '"30122"' + remote_user: '"root"' + gateway_host: '"123.59.135.93"' + gateway_port: '"51123"' + gateway_user: '"pdl"' + ssh_cmd: '"sh update.sh"' + needs: + - gitlink_scp_resource_0 - ref: end name: 结束 task: end needs: - - ssh_cmd_0 + - gitlink_ssh_cmd_0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6a6fa03 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM bitnami/git:latest +RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list +RUN apt update +RUN apt install -y vim nano curl wget +RUN wget --no-check-certificate https://go.dev/dl/go1.20.4.linux-amd64.tar.gz +RUN tar -C /usr/local -xzf go1.20.4.linux-amd64.tar.gz +ENV PATH=$PATH:/usr/local/go/bin +RUN go version +RUN go env -w GOPROXY=https://goproxy.cn +ADD ./ /gitea_hat/ +RUN cd /gitea_hat/ && ls -lh && sh build.sh + +#/gitea_hat/gitea_hat admin user create --username root --password 123456 --email root@forge.com --admin diff --git a/README.md b/README.md index 883dc85..3ca6da3 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,80 @@ -

- - Project logo - -

- -

Gitea GitLink版本

- -
- -[![Status](https://img.shields.io/badge/status-active-success.svg)]() -[![GitLink Issues](https://img.shields.io/github/issues/kylelobo/The-Documentation-Compendium.svg)](https://www.gitlink.org.cn/Gitlink/gitea_hat/issues) -[![GitLink Pull Requests](https://img.shields.io/github/issues-pr/kylelobo/The-Documentation-Compendium.svg)](https://www.gitlink.org.cn/Gitlink/gitea_hat/pulls) -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) - -
- ---- - -

此版本为GitLink平台使用的Git服务,该服务基于Gitea进行开发。 -
-

- -- [🧐 关于](#-关于) -- [编译](#编译) -- [运行](#运行) -- [接口文档](#接口文档) -- [⛏️ 使用](#️-使用) - -## 🧐 关于 - -该项目是为了方便平台后续Gitea版本的升级,将个性化接口部分从Gitea中抽离出来的项目,模块定义遵循Gitea规则,目前基于Gitea v1.18.5版本。 - -## 编译 - -类Linux平台,在项目根目录运行: -```shell -# 编译Linux 64位可执行程序 -CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o gitea main.go - -# 编译Windows 64位可执行程序 -CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o gitea main.go - -# 编译mac 64位可执行程序 -CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o gitea main.go -``` - -Win平台,在项目根目录运行: -```shell -# 编译Linux 64位可执行程序 -SET CGO_ENABLED=0 -SET GOOS=linux -SET GOARCH=amd64 -go build -o gitea main.go - -... -``` - -## 运行 - -在项目根目录下运行: - -``` -./gitea -p 10082 -``` - -## 接口文档 -- [ApiDoc](https://www.apifox.cn/apidoc/project-1809666) - 接口文档 - -## ⛏️ 使用 - -- [Gitea](https://gitea.io/zh-cn/) - Gitea +

+ + Project logo + +

+ +

Gitea GitLink版本

+ +
+ +[![Status](https://img.shields.io/badge/status-active-success.svg)]() +[![GitLink Issues](https://img.shields.io/github/issues/kylelobo/The-Documentation-Compendium.svg)](https://www.gitlink.org.cn/Gitlink/gitea_hat/issues) +[![GitLink Pull Requests](https://img.shields.io/github/issues-pr/kylelobo/The-Documentation-Compendium.svg)](https://www.gitlink.org.cn/Gitlink/gitea_hat/pulls) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) + +
+ +--- + +

此版本为GitLink平台使用的Git服务,该服务基于Gitea进行开发。 +
+

+ +- [🧐 关于](#-关于) +- [编译](#编译) +- [运行](#运行) +- [接口文档](#接口文档) +- [⛏️ 使用](#️-使用) + +## 🧐 关于 + +该项目是为了方便平台后续Gitea版本的升级,将个性化接口部分从Gitea中抽离出来的项目,模块定义遵循Gitea规则,目前基于Gitea v1.18.5版本。 + +## Go环境安装 +推荐1.20.x以上版本,下载https://go.dev/dl/ + +## 编译 + +类Linux平台,在项目根目录运行: +```shell +# 编译Linux 64位可执行程序 +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o gitea main.go + +# 编译Windows 64位可执行程序 +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o gitea main.go + +# 编译mac 64位可执行程序 +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o gitea main.go +``` + +Win平台,在项目根目录运行: +```shell +# 编译Linux 64位可执行程序 +SET CGO_ENABLED=0 +SET GOOS=linux +SET GOARCH=amd64 +go build -o gitea main.go + +... +``` + +## 运行 + +在项目根目录下运行: + +``` +./gitea -p 10082 +``` + +## 接口文档 +- [ApiDoc](https://www.apifox.cn/apidoc/project-1809666) - 接口文档 + +## ⛏️ 使用 + +- [Gitea](https://gitea.io/zh-cn/) - Gitea diff --git a/build.go b/build.go new file mode 100644 index 0000000..d410e17 --- /dev/null +++ b/build.go @@ -0,0 +1,14 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build vendor + +package main + +// Libraries that are included to vendor utilities used during build. +// These libraries will not be included in a normal compilation. + +import ( + // for embed + _ "github.com/shurcooL/vfsgen" +) diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..573317a --- /dev/null +++ b/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash +echo "go mod tidy..." +go mod tidy + +echo "go mod vendor..." +go mod vendor + +echo "go generate bindata.go for migration,options,public,templates..." +go run build/generate-bindata.go modules/schemas migration vendor/code.gitea.io/gitea/modules/migration/bindata.go +go run build/generate-bindata.go options options vendor/code.gitea.io/gitea/modules/options/bindata.go +go run build/generate-bindata.go public public vendor/code.gitea.io/gitea/modules/public/bindata.go +go run build/generate-bindata.go templates templates vendor/code.gitea.io/gitea/modules/templates/bindata.go + +echo "go build -tags 'bindata' -o gitea_hat main.go..." +go build -tags 'bindata' -o gitea_hat main.go diff --git a/build/generate-bindata.go b/build/generate-bindata.go new file mode 100644 index 0000000..efd172f --- /dev/null +++ b/build/generate-bindata.go @@ -0,0 +1,87 @@ +// Copyright 2020 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. + +//go:build ignore +// +build ignore + +package main + +import ( + "bytes" + "crypto/sha1" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "strconv" + + "github.com/shurcooL/vfsgen" +) + +func needsUpdate(dir string, filename string) (bool, []byte) { + needRegen := false + _, err := os.Stat(filename) + if err != nil { + needRegen = true + } + + oldHash, err := ioutil.ReadFile(filename + ".hash") + if err != nil { + oldHash = []byte{} + } + + hasher := sha1.New() + + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + _, _ = hasher.Write([]byte(info.Name())) + _, _ = hasher.Write([]byte(info.ModTime().String())) + _, _ = hasher.Write([]byte(strconv.FormatInt(info.Size(), 16))) + return nil + }) + if err != nil { + return true, oldHash + } + + newHash := hasher.Sum([]byte{}) + + if bytes.Compare(oldHash, newHash) != 0 { + + return true, newHash + } + + return needRegen, newHash +} + +func main() { + if len(os.Args) != 4 { + log.Fatal("Insufficient number of arguments. Need: directory packageName filename") + } + + dir, packageName, filename := os.Args[1], os.Args[2], os.Args[3] + + update, newHash := needsUpdate(dir, filename) + + if !update { + fmt.Printf("bindata for %s already up-to-date\n", packageName) + return + } + + fmt.Printf("generating bindata for %s\n", packageName) + var fsTemplates http.FileSystem = http.Dir(dir) + err := vfsgen.Generate(fsTemplates, vfsgen.Options{ + PackageName: packageName, + BuildTags: "bindata", + VariableName: "Assets", + Filename: filename, + }) + if err != nil { + log.Fatalf("%v\n", err) + } + _ = ioutil.WriteFile(filename+".hash", newHash, 0666) +} diff --git a/build/generate-emoji.go b/build/generate-emoji.go new file mode 100644 index 0000000..f8e6498 --- /dev/null +++ b/build/generate-emoji.go @@ -0,0 +1,225 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright 2015 Kenneth Shaw +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +//go:build ignore +// +build ignore + +package main + +import ( + "flag" + "fmt" + "go/format" + "io/ioutil" + "log" + "net/http" + "regexp" + "sort" + "strconv" + "strings" + "unicode/utf8" + + jsoniter "github.com/json-iterator/go" +) + +const ( + gemojiURL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json" + maxUnicodeVersion = 12 +) + +var ( + flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out") +) + +// Gemoji is a set of emoji data. +type Gemoji []Emoji + +// Emoji represents a single emoji and associated data. +type Emoji struct { + Emoji string `json:"emoji"` + Description string `json:"description,omitempty"` + Aliases []string `json:"aliases"` + UnicodeVersion string `json:"unicode_version,omitempty"` + SkinTones bool `json:"skin_tones,omitempty"` +} + +// Don't include some fields in JSON +func (e Emoji) MarshalJSON() ([]byte, error) { + type emoji Emoji + x := emoji(e) + x.UnicodeVersion = "" + x.Description = "" + x.SkinTones = false + json := jsoniter.ConfigCompatibleWithStandardLibrary + return json.Marshal(x) +} + +func main() { + var err error + + flag.Parse() + + // generate data + buf, err := generate() + if err != nil { + log.Fatal(err) + } + + // write + err = ioutil.WriteFile(*flagOut, buf, 0644) + if err != nil { + log.Fatal(err) + } +} + +var replacer = strings.NewReplacer( + "main.Gemoji", "Gemoji", + "main.Emoji", "\n", + "}}", "},\n}", + ", Description:", ", ", + ", Aliases:", ", ", + ", UnicodeVersion:", ", ", + ", SkinTones:", ", ", +) + +var emojiRE = regexp.MustCompile(`\{Emoji:"([^"]*)"`) + +func generate() ([]byte, error) { + var err error + + // load gemoji data + res, err := http.Get(gemojiURL) + if err != nil { + return nil, err + } + defer res.Body.Close() + + // read all + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + // unmarshal + var data Gemoji + json := jsoniter.ConfigCompatibleWithStandardLibrary + err = json.Unmarshal(body, &data) + if err != nil { + return nil, err + } + + var skinTones = make(map[string]string) + + skinTones["\U0001f3fb"] = "Light Skin Tone" + skinTones["\U0001f3fc"] = "Medium-Light Skin Tone" + skinTones["\U0001f3fd"] = "Medium Skin Tone" + skinTones["\U0001f3fe"] = "Medium-Dark Skin Tone" + skinTones["\U0001f3ff"] = "Dark Skin Tone" + + var tmp Gemoji + + //filter out emoji that require greater than max unicode version + for i := range data { + val, _ := strconv.ParseFloat(data[i].UnicodeVersion, 64) + if int(val) <= maxUnicodeVersion { + tmp = append(tmp, data[i]) + } + } + data = tmp + + sort.Slice(data, func(i, j int) bool { + return data[i].Aliases[0] < data[j].Aliases[0] + }) + + aliasMap := make(map[string]int, len(data)) + + for i, e := range data { + if e.Emoji == "" || len(e.Aliases) == 0 { + continue + } + for _, a := range e.Aliases { + if a == "" { + continue + } + aliasMap[a] = i + } + } + + // gitea customizations + i, ok := aliasMap["tada"] + if ok { + data[i].Aliases = append(data[i].Aliases, "hooray") + } + i, ok = aliasMap["laughing"] + if ok { + data[i].Aliases = append(data[i].Aliases, "laugh") + } + + // write a JSON file to use with tribute (write before adding skin tones since we can't support them there yet) + file, _ := json.Marshal(data) + _ = ioutil.WriteFile("assets/emoji.json", file, 0644) + + // Add skin tones to emoji that support it + var ( + s []string + newEmoji string + newDescription string + newData Emoji + ) + + for i := range data { + if data[i].SkinTones { + for k, v := range skinTones { + s = strings.Split(data[i].Emoji, "") + + if utf8.RuneCountInString(data[i].Emoji) == 1 { + s = append(s, k) + } else { + // insert into slice after first element because all emoji that support skin tones + // have that modifier placed at this spot + s = append(s, "") + copy(s[2:], s[1:]) + s[1] = k + } + + newEmoji = strings.Join(s, "") + newDescription = data[i].Description + ": " + v + newAlias := data[i].Aliases[0] + "_" + strings.ReplaceAll(v, " ", "_") + + newData = Emoji{newEmoji, newDescription, []string{newAlias}, "12.0", false} + data = append(data, newData) + } + } + } + + // add header + str := replacer.Replace(fmt.Sprintf(hdr, gemojiURL, data)) + + // change the format of the unicode string + str = emojiRE.ReplaceAllStringFunc(str, func(s string) string { + var err error + s, err = strconv.Unquote(s[len("{Emoji:"):]) + if err != nil { + panic(err) + } + return "{" + strconv.QuoteToASCII(s) + }) + + // format + return format.Source([]byte(str)) +} + +const hdr = ` +// Copyright 2020 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 emoji + +// Code generated by gen.go. DO NOT EDIT. +// Sourced from %s +// +var GemojiData = %#v +` diff --git a/build/generate-gitignores.go b/build/generate-gitignores.go new file mode 100644 index 0000000..d0b972e --- /dev/null +++ b/build/generate-gitignores.go @@ -0,0 +1,131 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "archive/tar" + "compress/gzip" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/util" +) + +func main() { + var ( + prefix = "gitea-gitignore" + url = "https://api.github.com/repos/github/gitignore/tarball" + githubApiToken = "" + githubUsername = "" + destination = "" + ) + + flag.StringVar(&destination, "dest", "options/gitignore/", "destination for the gitignores") + flag.StringVar(&githubUsername, "username", "", "github username") + flag.StringVar(&githubApiToken, "token", "", "github api token") + flag.Parse() + + file, err := ioutil.TempFile(os.TempDir(), prefix) + + if err != nil { + log.Fatalf("Failed to create temp file. %s", err) + } + + defer util.Remove(file.Name()) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatalf("Failed to download archive. %s", err) + } + + if len(githubApiToken) > 0 && len(githubUsername) > 0 { + req.SetBasicAuth(githubUsername, githubApiToken) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("Failed to download archive. %s", err) + } + defer resp.Body.Close() + + if _, err := io.Copy(file, resp.Body); err != nil { + log.Fatalf("Failed to copy archive to file. %s", err) + } + + if _, err := file.Seek(0, 0); err != nil { + log.Fatalf("Failed to reset seek on archive. %s", err) + } + + gz, err := gzip.NewReader(file) + + if err != nil { + log.Fatalf("Failed to gunzip the archive. %s", err) + } + + tr := tar.NewReader(gz) + + filesToCopy := make(map[string]string, 0) + + for { + hdr, err := tr.Next() + + if err == io.EOF { + break + } + + if err != nil { + log.Fatalf("Failed to iterate archive. %s", err) + } + + if filepath.Ext(hdr.Name) != ".gitignore" { + continue + } + + if hdr.Typeflag == tar.TypeSymlink { + fmt.Printf("Found symlink %s -> %s\n", hdr.Name, hdr.Linkname) + filesToCopy[strings.TrimSuffix(filepath.Base(hdr.Name), ".gitignore")] = strings.TrimSuffix(filepath.Base(hdr.Linkname), ".gitignore") + continue + } + + out, err := os.Create(path.Join(destination, strings.TrimSuffix(filepath.Base(hdr.Name), ".gitignore"))) + + if err != nil { + log.Fatalf("Failed to create new file. %s", err) + } + + defer out.Close() + + if _, err := io.Copy(out, tr); err != nil { + log.Fatalf("Failed to write new file. %s", err) + } else { + fmt.Printf("Written %s\n", out.Name()) + } + } + + for dst, src := range filesToCopy { + // Read all content of src to data + src = path.Join(destination, src) + data, err := ioutil.ReadFile(src) + if err != nil { + log.Fatalf("Failed to read src file. %s", err) + } + // Write data to dst + dst = path.Join(destination, dst) + err = ioutil.WriteFile(dst, data, 0644) + if err != nil { + log.Fatalf("Failed to write new file. %s", err) + } + fmt.Printf("Written (copy of %s) %s\n", src, dst) + } + + fmt.Println("Done") +} diff --git a/build/generate-images.js b/build/generate-images.js new file mode 100755 index 0000000..b661681 --- /dev/null +++ b/build/generate-images.js @@ -0,0 +1,83 @@ +import imageminZopfli from 'imagemin-zopfli'; +import {optimize, extendDefaultPlugins} from 'svgo'; +import {fabric} from 'fabric'; +import fs from 'fs'; +import {resolve, dirname} from 'path'; +import {fileURLToPath} from 'url'; + +const {readFile, writeFile} = fs.promises; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const logoFile = resolve(__dirname, '../assets/logo.svg'); + +function exit(err) { + if (err) console.error(err); + process.exit(err ? 1 : 0); +} + +function loadSvg(svg) { + return new Promise((resolve) => { + fabric.loadSVGFromString(svg, (objects, options) => { + resolve({objects, options}); + }); + }); +} + +async function generate(svg, outputFile, {size, bg}) { + if (outputFile.endsWith('.svg')) { + const {data} = optimize(svg, { + plugins: extendDefaultPlugins([ + 'removeDimensions', + { + name: 'addAttributesToSVGElement', + params: {attributes: [{width: size}, {height: size}]} + }, + ]), + }); + await writeFile(outputFile, data); + return; + } + + const {objects, options} = await loadSvg(svg); + const canvas = new fabric.Canvas(); + canvas.setDimensions({width: size, height: size}); + const ctx = canvas.getContext('2d'); + ctx.scale(options.width ? (size / options.width) : 1, options.height ? (size / options.height) : 1); + + if (bg) { + canvas.add(new fabric.Rect({ + left: 0, + top: 0, + height: size * (1 / (size / options.height)), + width: size * (1 / (size / options.width)), + fill: 'white', + })); + } + + canvas.add(fabric.util.groupSVGElements(objects, options)); + canvas.renderAll(); + + let png = Buffer.from([]); + for await (const chunk of canvas.createPNGStream()) { + png = Buffer.concat([png, chunk]); + } + + png = await imageminZopfli({more: true})(png); + await writeFile(outputFile, png); +} + +async function main() { + const gitea = process.argv.slice(2).includes('gitea'); + const svg = await readFile(logoFile, 'utf8'); + + await Promise.all([ + generate(svg, resolve(__dirname, '../public/img/logo.svg'), {size: 32}), + generate(svg, resolve(__dirname, '../public/img/logo.png'), {size: 512}), + generate(svg, resolve(__dirname, '../public/img/favicon.png'), {size: 180}), + generate(svg, resolve(__dirname, '../public/img/avatar_default.png'), {size: 200}), + generate(svg, resolve(__dirname, '../public/img/apple-touch-icon.png'), {size: 180, bg: true}), + gitea && generate(svg, resolve(__dirname, '../public/img/gitea.svg'), {size: 32}), + ]); +} + +main().then(exit).catch(exit); + diff --git a/build/generate-licenses.go b/build/generate-licenses.go new file mode 100644 index 0000000..4009a03 --- /dev/null +++ b/build/generate-licenses.go @@ -0,0 +1,119 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "archive/tar" + "compress/gzip" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/util" +) + +func main() { + var ( + prefix = "gitea-licenses" + url = "https://api.github.com/repos/spdx/license-list-data/tarball" + githubApiToken = "" + githubUsername = "" + destination = "" + ) + + flag.StringVar(&destination, "dest", "options/license/", "destination for the licenses") + flag.StringVar(&githubUsername, "username", "", "github username") + flag.StringVar(&githubApiToken, "token", "", "github api token") + flag.Parse() + + file, err := ioutil.TempFile(os.TempDir(), prefix) + + if err != nil { + log.Fatalf("Failed to create temp file. %s", err) + } + + defer util.Remove(file.Name()) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatalf("Failed to download archive. %s", err) + } + + if len(githubApiToken) > 0 && len(githubUsername) > 0 { + req.SetBasicAuth(githubUsername, githubApiToken) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("Failed to download archive. %s", err) + } + + defer resp.Body.Close() + + if _, err := io.Copy(file, resp.Body); err != nil { + log.Fatalf("Failed to copy archive to file. %s", err) + } + + if _, err := file.Seek(0, 0); err != nil { + log.Fatalf("Failed to reset seek on archive. %s", err) + } + + gz, err := gzip.NewReader(file) + + if err != nil { + log.Fatalf("Failed to gunzip the archive. %s", err) + } + + tr := tar.NewReader(gz) + + for { + hdr, err := tr.Next() + + if err == io.EOF { + break + } + + if err != nil { + log.Fatalf("Failed to iterate archive. %s", err) + } + + if !strings.Contains(hdr.Name, "/text/") { + continue + } + + if filepath.Ext(hdr.Name) != ".txt" { + continue + } + + if strings.HasPrefix(filepath.Base(hdr.Name), "README") { + continue + } + + if strings.HasPrefix(filepath.Base(hdr.Name), "deprecated_") { + continue + } + out, err := os.Create(path.Join(destination, strings.TrimSuffix(filepath.Base(hdr.Name), ".txt"))) + + if err != nil { + log.Fatalf("Failed to create new file. %s", err) + } + + defer out.Close() + + if _, err := io.Copy(out, tr); err != nil { + log.Fatalf("Failed to write new file. %s", err) + } else { + fmt.Printf("Written %s\n", out.Name()) + } + } + + fmt.Println("Done") +} diff --git a/build/generate-svg.js b/build/generate-svg.js new file mode 100755 index 0000000..cde3f31 --- /dev/null +++ b/build/generate-svg.js @@ -0,0 +1,63 @@ +import fastGlob from 'fast-glob'; +import {optimize, extendDefaultPlugins} from 'svgo'; +import {resolve, parse, dirname} from 'path'; +import fs from 'fs'; +import {fileURLToPath} from 'url'; + +const {readFile, writeFile, mkdir} = fs.promises; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const glob = (pattern) => fastGlob.sync(pattern, {cwd: resolve(__dirname), absolute: true}); +const outputDir = resolve(__dirname, '../public/img/svg'); + +function exit(err) { + if (err) console.error(err); + process.exit(err ? 1 : 0); +} + +async function processFile(file, {prefix, fullName} = {}) { + let name; + + if (fullName) { + name = fullName; + } else { + name = parse(file).name; + if (prefix) name = `${prefix}-${name}`; + if (prefix === 'octicon') name = name.replace(/-[0-9]+$/, ''); // chop of '-16' on octicons + } + + const {data} = optimize(await readFile(file, 'utf8'), { + plugins: extendDefaultPlugins([ + 'removeXMLNS', + 'removeDimensions', + {name: 'prefixIds', params: {prefix: () => name}}, + { + name: 'addClassesToSVGElement', + params: {classNames: ['svg', name]}, + }, + { + name: 'addAttributesToSVGElement', + params: {attributes: [{'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'}]}, + }, + ]), + }); + await writeFile(resolve(outputDir, `${name}.svg`), data); +} + +function processFiles(pattern, opts) { + return glob(pattern).map((file) => processFile(file, opts)); +} + +async function main() { + try { + await mkdir(outputDir); + } catch {} + + await Promise.all([ + ...processFiles('../node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}), + ...processFiles('../web_src/svg/*.svg'), + ...processFiles('../public/img/gitea.svg', {fullName: 'gitea-gitea'}), + ]); +} + +main().then(exit).catch(exit); + diff --git a/build/gocovmerge.go b/build/gocovmerge.go new file mode 100644 index 0000000..b38cf5e --- /dev/null +++ b/build/gocovmerge.go @@ -0,0 +1,120 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright (c) 2015, Wade Simmons +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +// gocovmerge takes the results from multiple `go test -coverprofile` runs and +// merges them into one profile + +//go:build ignore +// +build ignore + +package main + +import ( + "flag" + "fmt" + "io" + "log" + "os" + "sort" + + "golang.org/x/tools/cover" +) + +func mergeProfiles(p *cover.Profile, merge *cover.Profile) { + if p.Mode != merge.Mode { + log.Fatalf("cannot merge profiles with different modes") + } + // Since the blocks are sorted, we can keep track of where the last block + // was inserted and only look at the blocks after that as targets for merge + startIndex := 0 + for _, b := range merge.Blocks { + startIndex = mergeProfileBlock(p, b, startIndex) + } +} + +func mergeProfileBlock(p *cover.Profile, pb cover.ProfileBlock, startIndex int) int { + sortFunc := func(i int) bool { + pi := p.Blocks[i+startIndex] + return pi.StartLine >= pb.StartLine && (pi.StartLine != pb.StartLine || pi.StartCol >= pb.StartCol) + } + + i := 0 + if sortFunc(i) != true { + i = sort.Search(len(p.Blocks)-startIndex, sortFunc) + } + i += startIndex + if i < len(p.Blocks) && p.Blocks[i].StartLine == pb.StartLine && p.Blocks[i].StartCol == pb.StartCol { + if p.Blocks[i].EndLine != pb.EndLine || p.Blocks[i].EndCol != pb.EndCol { + log.Fatalf("OVERLAP MERGE: %v %v %v", p.FileName, p.Blocks[i], pb) + } + switch p.Mode { + case "set": + p.Blocks[i].Count |= pb.Count + case "count", "atomic": + p.Blocks[i].Count += pb.Count + default: + log.Fatalf("unsupported covermode: '%s'", p.Mode) + } + } else { + if i > 0 { + pa := p.Blocks[i-1] + if pa.EndLine >= pb.EndLine && (pa.EndLine != pb.EndLine || pa.EndCol > pb.EndCol) { + log.Fatalf("OVERLAP BEFORE: %v %v %v", p.FileName, pa, pb) + } + } + if i < len(p.Blocks)-1 { + pa := p.Blocks[i+1] + if pa.StartLine <= pb.StartLine && (pa.StartLine != pb.StartLine || pa.StartCol < pb.StartCol) { + log.Fatalf("OVERLAP AFTER: %v %v %v", p.FileName, pa, pb) + } + } + p.Blocks = append(p.Blocks, cover.ProfileBlock{}) + copy(p.Blocks[i+1:], p.Blocks[i:]) + p.Blocks[i] = pb + } + return i + 1 +} + +func addProfile(profiles []*cover.Profile, p *cover.Profile) []*cover.Profile { + i := sort.Search(len(profiles), func(i int) bool { return profiles[i].FileName >= p.FileName }) + if i < len(profiles) && profiles[i].FileName == p.FileName { + mergeProfiles(profiles[i], p) + } else { + profiles = append(profiles, nil) + copy(profiles[i+1:], profiles[i:]) + profiles[i] = p + } + return profiles +} + +func dumpProfiles(profiles []*cover.Profile, out io.Writer) { + if len(profiles) == 0 { + return + } + fmt.Fprintf(out, "mode: %s\n", profiles[0].Mode) + for _, p := range profiles { + for _, b := range p.Blocks { + fmt.Fprintf(out, "%s:%d.%d,%d.%d %d %d\n", p.FileName, b.StartLine, b.StartCol, b.EndLine, b.EndCol, b.NumStmt, b.Count) + } + } +} + +func main() { + flag.Parse() + + var merged []*cover.Profile + + for _, file := range flag.Args() { + profiles, err := cover.ParseProfiles(file) + if err != nil { + log.Fatalf("failed to parse profiles: %v", err) + } + for _, p := range profiles { + merged = addProfile(merged, p) + } + } + + dumpProfiles(merged, os.Stdout) +} diff --git a/build/update-locales.sh b/build/update-locales.sh new file mode 100755 index 0000000..046f48e --- /dev/null +++ b/build/update-locales.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +mv ./options/locale/locale_en-US.ini ./options/ + +# Make sure to only change lines that have the translation enclosed between quotes +sed -i -r -e '/^[a-zA-Z0-9_.-]+[ ]*=[ ]*".*"$/ { + s/^([a-zA-Z0-9_.-]+)[ ]*="/\1=/ + s/\\"/"/g + s/"$// + }' ./options/locale/*.ini + +# Remove translation under 25% of en_us +baselines=$(wc -l "./options/locale_en-US.ini" | cut -d" " -f1) +baselines=$((baselines / 4)) +for filename in ./options/locale/*.ini; do + lines=$(wc -l "$filename" | cut -d" " -f1) + if [ $lines -lt $baselines ]; then + echo "Removing $filename: $lines/$baselines" + rm "$filename" + fi +done + +mv ./options/locale_en-US.ini ./options/locale/ diff --git a/go.mod b/go.mod index 5c36cf0..51b38b7 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/klauspost/cpuid/v2 v2.2.3 github.com/russross/blackfriday/v2 v2.1.0 + github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 github.com/urfave/cli v1.22.12 golang.org/x/net v0.8.0 golang.org/x/text v0.8.0 @@ -180,6 +181,7 @@ require ( github.com/rs/xid v1.4.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.2.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect + github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/skeema/knownhosts v1.1.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect diff --git a/go.sum b/go.sum index 74eaa0a..f6e54ca 100644 --- a/go.sum +++ b/go.sum @@ -1119,7 +1119,11 @@ github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NF github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvqWEUH6SjNiu7VhSjuVFTFiTcphaLU= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= github.com/siddontang/go-snappy v0.0.0-20140704025258-d8f7bb82a96d/go.mod h1:vq0tzqLRu6TS7Id0wMo2N5QzJoKedVeovOpHjnykSzY= github.com/siddontang/ledisdb v0.0.0-20190202134119-8ceb77e66a92/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg= diff --git a/modules/convert/git_commit.go b/modules/convert/git_commit.go index 7c63263..c9ac248 100644 --- a/modules/convert/git_commit.go +++ b/modules/convert/git_commit.go @@ -3,13 +3,16 @@ package convert import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - gitea_convert "code.gitea.io/gitea/modules/convert" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs" + "net/url" + "time" ) func ToCommit(repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, stat bool) (*hat_api.Commit, error) { - giteaApiCommit, err := gitea_convert.ToCommit(repo, gitRepo, commit, userCache, stat) + giteaApiCommit, err := ToCommitNotDiff(repo, gitRepo, commit, userCache, stat) if err != nil { return nil, err } @@ -24,3 +27,132 @@ func ToCommit(repo *repo_model.Repository, gitRepo *git.Repository, commit *git. Branch: commit.Branch, }, nil } + +func ToCommitNotDiff(repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, stat bool) (*api.Commit, error) { + var apiAuthor, apiCommitter *api.User + + // Retrieve author and committer information + + var cacheAuthor *user_model.User + var ok bool + if userCache == nil { + cacheAuthor = (*user_model.User)(nil) + ok = false + } else { + cacheAuthor, ok = userCache[commit.Author.Email] + } + + if ok { + apiAuthor = convert.ToUser(cacheAuthor, nil) + } else { + author, err := user_model.GetUserByEmail(commit.Author.Email) + if err != nil && !user_model.IsErrUserNotExist(err) { + return nil, err + } else if err == nil { + apiAuthor = convert.ToUser(author, nil) + if userCache != nil { + userCache[commit.Author.Email] = author + } + } + } + + var cacheCommitter *user_model.User + if userCache == nil { + cacheCommitter = (*user_model.User)(nil) + ok = false + } else { + cacheCommitter, ok = userCache[commit.Committer.Email] + } + + if ok { + apiCommitter = convert.ToUser(cacheCommitter, nil) + } else { + committer, err := user_model.GetUserByEmail(commit.Committer.Email) + if err != nil && !user_model.IsErrUserNotExist(err) { + return nil, err + } else if err == nil { + apiCommitter = convert.ToUser(committer, nil) + if userCache != nil { + userCache[commit.Committer.Email] = committer + } + } + } + + // Retrieve parent(s) of the commit + apiParents := make([]*api.CommitMeta, commit.ParentCount()) + for i := 0; i < commit.ParentCount(); i++ { + sha, _ := commit.ParentID(i) + apiParents[i] = &api.CommitMeta{ + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(sha.String()), + SHA: sha.String(), + } + } + + res := &api.Commit{ + CommitMeta: &api.CommitMeta{ + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()), + SHA: commit.ID.String(), + Created: commit.Committer.When, + }, + HTMLURL: repo.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()), + RepoCommit: &api.RepoCommit{ + URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()), + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: commit.Author.Name, + Email: commit.Author.Email, + }, + Date: commit.Author.When.Format(time.RFC3339), + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: commit.Committer.Name, + Email: commit.Committer.Email, + }, + Date: commit.Committer.When.Format(time.RFC3339), + }, + Message: commit.Message(), + Tree: &api.CommitMeta{ + URL: repo.APIURL() + "/git/trees/" + url.PathEscape(commit.ID.String()), + SHA: commit.ID.String(), + Created: commit.Committer.When, + }, + Verification: convert.ToVerification(commit), + }, + Author: apiAuthor, + Committer: apiCommitter, + Parents: apiParents, + } + + // Retrieve files affected by the commit + if stat { + fileStatus, err := git.GetCommitFileStatus(gitRepo.Ctx, repo.RepoPath(), commit.ID.String()) + if err != nil { + return nil, err + } + affectedFileList := make([]*api.CommitAffectedFiles, 0, len(fileStatus.Added)+len(fileStatus.Removed)+len(fileStatus.Modified)) + for _, files := range [][]string{fileStatus.Added, fileStatus.Removed, fileStatus.Modified} { + for _, filename := range files { + affectedFileList = append(affectedFileList, &api.CommitAffectedFiles{ + Filename: filename, + }) + } + } + + //diff, err := gitdiff.GetDiff(gitRepo, &gitdiff.DiffOptions{ + // AfterCommitID: commit.ID.String(), + //}) + //if err != nil { + // return nil, err + //} + + res.Files = affectedFileList + //res.Stats = &api.CommitStats{ + // Total: diff.TotalAddition + diff.TotalDeletion, + // Additions: diff.TotalAddition, + // Deletions: diff.TotalDeletion, + //} + } + + return res, nil +} diff --git a/modules/git/repo_contributors.go b/modules/git/repo_contributors.go index 8d2a566..e75b532 100644 --- a/modules/git/repo_contributors.go +++ b/modules/git/repo_contributors.go @@ -82,3 +82,82 @@ func GetRepoContributors(repo *gitea_git.Repository, page, pageSize int) (int, [ return total, contributors, nil } + +func GetRepoContributorsNew(repo *gitea_git.Repository, page, pageSize int) (int, []*RepoContributor, error) { + var total, totalContributions, skip int + var contributors []*RepoContributor + var contributorInfos []*RepoContributor + contributorInfoHash := make(map[string]*RepoContributor) + + skip = (page - 1) * pageSize + + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return total, nil, err + } + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + cmd := gitea_git.NewCommand(repo.Ctx, "shortlog", "-sne", "--all") + + stderr := new(strings.Builder) + err = cmd.Run(&gitea_git.RunOpts{ + Env: []string{}, + Dir: repo.Path, + Stdout: stdoutWriter, + Stderr: stderr, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + scanner := bufio.NewScanner(stdoutReader) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + l := strings.TrimSpace(scanner.Text()) + commits := l[0:strings.Index(l, "\t")] + commitsInt, err := strconv.Atoi(commits) + if err != nil { + return err + } + name := l[strings.Index(l, "\t")+1 : strings.Index(l, " <")] + email := l[strings.Index(l, "<")+1 : strings.Index(l, ">")] + totalContributions += commitsInt + // committer is not system user + if existedContributorInfo, ok := contributorInfoHash[email]; ok { + // existed: same primary email, different committer name + existedContributorInfo.Commits += commitsInt + } else { + var newContributor = &RepoContributor{ + Commits: commitsInt, + Name: name, + Email: email, + } + total++ + contributorInfos = append(contributorInfos, newContributor) + contributorInfoHash[email] = newContributor + } + } + + _ = stdoutReader.Close() + return nil + }, + }) + + var ca int + for _, cont := range contributorInfos { + if skip > 0 { + skip-- + } else { + if ca < pageSize { + fperc := fmt.Sprintf("%.2f", float64(cont.Commits)*100/float64(totalContributions)) + contributors = append(contributors, &RepoContributor{ + Commits: cont.Commits, + Name: cont.Name, + Email: cont.Email, + ContributionPerc: fperc + "%", + }) + ca++ + } + } + } + return total, contributors, nil +} diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index d348b26..dac957f 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -8,6 +8,7 @@ import ( "sort" "strconv" "strings" + "time" "code.gitea.io/gitea/modules/container" gitea_git "code.gitea.io/gitea/modules/git" @@ -145,11 +146,12 @@ func GetCodeActivityStatsWithoutSince(repo *gitea_git.Repository, branch string) } // GetCodeActivityStats returns code statistics for activity page -func GetPaginateCodeAuthorsWithoutSince(repo *gitea_git.Repository, branch string, page, pageSize int) (int64, []*CodeActivityAuthor, error) { +func GetPaginateCodeAuthors(repo *gitea_git.Repository, fromTime time.Time, branch string, page, pageSize int) (int64, []*CodeActivityAuthor, error) { var total int64 var authors []*CodeActivityAuthor + since := fromTime.Format(time.RFC3339) - authorCmd := gitea_git.NewCommand(repo.Ctx, "log", "--no-merges", "--format=%aN <%aE>", "--date=iso") + authorCmd := gitea_git.NewCommand(repo.Ctx, "log", "--no-merges", "--format=%aN <%aE>", "--date=iso", gitea_git.CmdArg(fmt.Sprintf("--since='%s'", since))) if len(branch) == 0 { authorCmd.AddArguments("--branches=*") } else { @@ -199,7 +201,7 @@ func GetPaginateCodeAuthorsWithoutSince(repo *gitea_git.Repository, branch strin _ = stdoutReader.Close() _ = stdoutWriter.Close() }() - gitCmd := gitea_git.NewCommand(repo.Ctx, "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso", gitea_git.CmdArg(fmt.Sprintf("--author=%s", filterAuthor))) + gitCmd := gitea_git.NewCommand(repo.Ctx, "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso", gitea_git.CmdArg(fmt.Sprintf("--author=%s", filterAuthor)), gitea_git.CmdArg(fmt.Sprintf("--since='%s'", since))) if len(branch) == 0 { gitCmd.AddArguments("--branches=*") } else { diff --git a/modules/schemas/issue.json b/modules/schemas/issue.json new file mode 100644 index 0000000..25753c3 --- /dev/null +++ b/modules/schemas/issue.json @@ -0,0 +1,114 @@ +{ + "title": "Issue", + "description": "Issues associated to a repository within a forge (Gitea, GitLab, etc.).", + + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "number": { + "description": "Unique identifier, relative to the repository.", + "type": "number" + }, + "poster_id": { + "description": "Unique identifier of the user who authored the issue.", + "type": "number" + }, + "poster_name": { + "description": "Name of the user who authored the issue.", + "type": "string" + }, + "poster_email": { + "description": "Email of the user who authored the issue.", + "type": "string" + }, + "title": { + "description": "Short description displayed as the title.", + "type": "string" + }, + "content": { + "description": "Long, multiline, description.", + "type": "string" + }, + "ref": { + "description": "Target branch in the repository.", + "type": "string" + }, + "milestone": { + "description": "Name of the milestone.", + "type": "string" + }, + "state": { + "description": "A 'closed' issue will not see any activity in the future, otherwise it is 'open'.", + "enum": [ + "closed", + "open" + ] + }, + "is_locked": { + "description": "A locked issue can only be modified by privileged users.", + "type": "boolean" + }, + "created": { + "description": "Creation time.", + "type": "string", + "format": "date-time" + }, + "updated": { + "description": "Last update time.", + "type": "string", + "format": "date-time" + }, + "closed": { + "description": "The last time 'state' changed to 'closed'.", + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "labels": { + "description": "List of labels.", + "type": "array", + "items": { + "$ref": "label.json" + } + }, + "reactions": { + "description": "List of reactions.", + "type": "array", + "items": { + "$ref": "reaction.json" + } + }, + "assignees": { + "description": "List of assignees.", + "type": "array", + "items": { + "description": "Name of a user assigned to the issue.", + "type": "string" + } + } + }, + "required": [ + "number", + "poster_id", + "poster_name", + "title", + "content", + "state", + "is_locked", + "created", + "updated" + ] + }, + + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "http://example.com/issue.json", + "$$target": "issue.json" +} diff --git a/modules/schemas/label.json b/modules/schemas/label.json new file mode 100644 index 0000000..561a2e3 --- /dev/null +++ b/modules/schemas/label.json @@ -0,0 +1,28 @@ +{ + "title": "Label", + "description": "Label associated to an issue.", + + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of the label, unique within the repository.", + "type": "string" + }, + "color": { + "description": "Color code of the label.", + "type": "string" + }, + "description": { + "description": "Long, multiline, description.", + "type": "string" + } + }, + "required": [ + "name" + ], + + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "label.json", + "$$target": "label.json" +} diff --git a/modules/schemas/milestone.json b/modules/schemas/milestone.json new file mode 100644 index 0000000..7024ef4 --- /dev/null +++ b/modules/schemas/milestone.json @@ -0,0 +1,67 @@ +{ + "title": "Milestone", + "description": "Milestone associated to a repository within a forge.", + + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "description": "Short description.", + "type": "string" + }, + "description": { + "description": "Long, multiline, description.", + "type": "string" + }, + "deadline": { + "description": "Deadline after which the milestone is overdue.", + "type": "string", + "format": "date-time" + }, + "created": { + "description": "Creation time.", + "type": "string", + "format": "date-time" + }, + "updated": { + "description": "Last update time.", + "type": "string", + "format": "date-time" + }, + "closed": { + "description": "The last time 'state' changed to 'closed'.", + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "state": { + "description": "A 'closed' issue will not see any activity in the future, otherwise it is 'open'.", + "enum": [ + "closed", + "open" + ] + } + }, + "required": [ + "title", + "description", + "deadline", + "created", + "updated", + "closed", + "state" + ] + }, + + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "http://example.com/milestone.json", + "$$target": "milestone.json" +} diff --git a/modules/schemas/reaction.json b/modules/schemas/reaction.json new file mode 100644 index 0000000..2565251 --- /dev/null +++ b/modules/schemas/reaction.json @@ -0,0 +1,29 @@ +{ + "title": "Reaction", + "description": "Reaction associated to an issue or a comment.", + + "type": "object", + "additionalProperties": false, + "properties": { + "user_id": { + "description": "Unique identifier of the user who authored the reaction.", + "type": "number" + }, + "user_name": { + "description": "Name of the user who authored the reaction.", + "type": "string" + }, + "content": { + "description": "Representation of the reaction", + "type": "string" + } + }, + "required": [ + "user_id", + "content" + ], + + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "http://example.com/reaction.json", + "$$target": "reaction.json" +} diff --git a/routers/hat/hat.go b/routers/hat/hat.go index 1e247b4..539d108 100644 --- a/routers/hat/hat.go +++ b/routers/hat/hat.go @@ -119,6 +119,7 @@ func Routers(ctx gocontext.Context) *web.Route { }, reqRepoReader(unit.TypeReleases)) m.Group("/contributors", func() { m.Get("", context.ReferencesGitRepo(), repo.GetContributors) + m.Get("/stat", context.ReferencesGitRepo(), repo.GetContributorStat) }) m.Group("/count", func() { m.Get("", context.ReferencesGitRepo(), repo.GetCommitCount) diff --git a/routers/hat/repo/commits.go b/routers/hat/repo/commits.go index fd3698f..ff93fe5 100644 --- a/routers/hat/repo/commits.go +++ b/routers/hat/repo/commits.go @@ -87,8 +87,10 @@ func GetAllCommitsSliceByTime(ctx *context.APIContext) { apiCommits := make([]*responseCommit, len(commits)) apiCommitsList := []responseCommit{} + stat := ctx.FormString("stat") == "" || ctx.FormBool("stat") + for i, commitPoniter := range commits { - apiCommits[i], err = toResponseCommit(ctx.Repo.Repository, gitRepo, commitPoniter, userCache) + apiCommits[i], err = toResponseCommit(ctx.Repo.Repository, gitRepo, commitPoniter, userCache, stat) if err != nil { ctx.Error(http.StatusInternalServerError, "ToCommit", err) return @@ -114,8 +116,8 @@ type responseCommit struct { CommitDate string `json:"commit_date"` } -func toResponseCommit(repo *repo.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user.User) (*responseCommit, error) { - apiCommit, err := convert.ToCommit(repo, gitRepo, commit, userCache, true) +func toResponseCommit(repo *repo.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user.User, stat bool) (*responseCommit, error) { + apiCommit, err := convert.ToCommit(repo, gitRepo, commit, userCache, stat) if err != nil { return nil, err } @@ -190,8 +192,10 @@ func GetFileAllCommits(ctx *context.APIContext) { userCache := make(map[string]*user_model.User) apiCommits := make([]*api.Commit, len(commits)) + + stat := ctx.FormString("stat") == "" || ctx.FormBool("stat") for i, commit := range commits { - apiCommits[i], err = convert.ToCommit(ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, true) + apiCommits[i], err = hat_convert.ToCommitNotDiff(ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, stat) if err != nil { ctx.Error(http.StatusInternalServerError, "ToCommit", err) return diff --git a/routers/hat/repo/repo.go b/routers/hat/repo/repo.go index 8e206ba..ad05c48 100644 --- a/routers/hat/repo/repo.go +++ b/routers/hat/repo/repo.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strings" + "time" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -484,7 +485,7 @@ func SetDiffViewStyle(ctx *context.Context) { func GetContributors(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - total, list, err := hat_git.GetRepoContributors(ctx.Repo.GitRepo, listOptions.Page, listOptions.PageSize) + total, list, err := hat_git.GetRepoContributorsNew(ctx.Repo.GitRepo, listOptions.Page, listOptions.PageSize) if err != nil { ctx.Error(http.StatusInternalServerError, "GetRepoContributors", err) return @@ -498,6 +499,31 @@ func GetContributors(ctx *context.APIContext) { ctx.JSON(http.StatusOK, list) } +func GetContributorStat(ctx *context.APIContext) { + listOptions := utils.GetListOptions(ctx) + + ref := ctx.FormString("ref") + year := ctx.FormInt("pass_year") + var timeFrom time.Time = time.Now().AddDate(-1, 0, 0) // 默认近一年 + + if year > 0 { + timeFrom = time.Now().AddDate(-year, 0, 0) // 近几年 + } + + total, list, err := hat_git.GetPaginateCodeAuthors(ctx.Repo.GitRepo, timeFrom, ref, listOptions.Page, listOptions.PageSize) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetPaginateCodeAuthors", err) + return + } + + ctx.SetLinkHeader(int(total), listOptions.PageSize) + ctx.RespHeader().Set("X-Total", fmt.Sprintf("%d", total)) + ctx.RespHeader().Set("X-Total-Count", fmt.Sprintf("%d", total)) + ctx.RespHeader().Set("Access-Control-Expose-Headers", "X-Total-Count, Link, X-Total") + + ctx.JSON(http.StatusOK, list) +} + type CountDTO struct { Branch CountDTOBranch `json:"branch"` ReleaseCount int64 `json:"release_count"`