You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2022/07/06 01:01:07 UTC
[incubator-devlake] branch main updated: [feat-1022][github]: Support GitHub Milestones (#2215)
This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new 74226840 [feat-1022][github]: Support GitHub Milestones (#2215)
74226840 is described below
commit 74226840d5dd64200c1b8a3953589dd37efa2c65
Author: Keon Amini <ke...@gmail.com>
AuthorDate: Tue Jul 5 19:01:03 2022 -0600
[feat-1022][github]: Support GitHub Milestones (#2215)
* feat(github): added support for github milestones (#1022)
* test: added unit tests to data_flow_tester
* test: improved the added tests
Co-authored-by: Keon Amini <ke...@merico.dev>
---
config-ui/src/hooks/useConnectionManager.jsx | 2 +-
config/config.go | 8 +-
go.mod | 6 +-
go.sum | 5 +-
helpers/e2ehelper/data_flow_tester.go | 117 ++++++++--
helpers/e2ehelper/data_flow_tester_test.go | 153 +++++++++++++
plugins/github/e2e/issue_test.go | 1 +
plugins/github/e2e/milestone_test.go | 76 +++++++
.../e2e/raw_tables/_raw_github_api_issues.csv | 84 +++----
.../e2e/raw_tables/_raw_github_api_milestones.csv | 2 +
.../e2e/snapshot_tables/_tool_github_issues.csv | 54 ++---
.../snapshot_tables/_tool_github_milestones.csv | 2 +
.../github/e2e/snapshot_tables/board_sprint.csv | 2 +
plugins/github/e2e/snapshot_tables/boards.csv | 2 +-
.../github/e2e/snapshot_tables/sprint_issue.csv | 27 +++
plugins/github/e2e/snapshot_tables/sprints.csv | 2 +
plugins/github/impl/impl.go | 8 +-
plugins/github/models/issue.go | 1 +
plugins/github/models/migrationscripts/register.go | 5 +-
.../migrationscripts/updateSchemas20220620.go | 73 +++++++
.../{migrationscripts/register.go => milestone.go} | 30 ++-
plugins/github/tasks/issue_collector.go | 2 +-
plugins/github/tasks/issue_extractor.go | 241 ++++++++++++---------
plugins/github/tasks/milestone_collector.go | 78 +++++++
plugins/github/tasks/milestone_converter.go | 109 ++++++++++
plugins/github/tasks/milestone_extractor.go | 118 ++++++++++
runner/directrun.go | 56 +++--
27 files changed, 1034 insertions(+), 230 deletions(-)
diff --git a/config-ui/src/hooks/useConnectionManager.jsx b/config-ui/src/hooks/useConnectionManager.jsx
index c4d8eeda..56917613 100644
--- a/config-ui/src/hooks/useConnectionManager.jsx
+++ b/config-ui/src/hooks/useConnectionManager.jsx
@@ -86,7 +86,7 @@ function useConnectionManager ({
connectionPayload = { endpoint: endpointUrl, username: username, password: password, proxy: proxy }
break
case Providers.GITHUB:
- connectionPayload = { endpoint: endpointUrl, auth: token, proxy: proxy }
+ connectionPayload = { endpoint: endpointUrl, token: token, proxy: proxy }
break
case Providers.JENKINS:
connectionPayload = { endpoint: endpointUrl, username: username, password: password }
diff --git a/config/config.go b/config/config.go
index ec2edc07..734db1a7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -46,10 +46,10 @@ func initConfig(v *viper.Viper) {
v.SetConfigType("env")
envPath := getEnvPath()
// AddConfigPath adds a path for Viper to search for the config file in.
- v.AddConfigPath("$PWD/../..")
- v.AddConfigPath("$PWD/../../..")
- v.AddConfigPath("..")
- v.AddConfigPath(".")
+ v.AddConfigPath("./../../..")
+ v.AddConfigPath("./../..")
+ v.AddConfigPath("./../")
+ v.AddConfigPath("./")
v.AddConfigPath(envPath)
}
diff --git a/go.mod b/go.mod
index 38ce2129..710d9139 100644
--- a/go.mod
+++ b/go.mod
@@ -3,13 +3,13 @@ module github.com/apache/incubator-devlake
go 1.17
require (
- github.com/agiledragon/gomonkey/v2 v2.7.0
github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.7.7
github.com/go-git/go-git/v5 v5.4.2
github.com/go-playground/validator/v10 v10.9.0
github.com/libgit2/git2go/v33 v33.0.6
github.com/magiconair/properties v1.8.5
+ github.com/manifoldco/promptui v0.9.0
github.com/mitchellh/mapstructure v1.4.1
github.com/panjf2000/ants/v2 v2.4.6
github.com/robfig/cron/v3 v3.0.0
@@ -17,6 +17,7 @@ require (
github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.5.0
github.com/spf13/viper v1.8.1
+ github.com/stoewer/go-strcase v1.2.0
github.com/stretchr/testify v1.7.0
github.com/swaggo/gin-swagger v1.4.3
github.com/swaggo/swag v1.8.1
@@ -37,6 +38,7 @@ require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
+ github.com/agiledragon/gomonkey/v2 v2.7.0 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -81,7 +83,6 @@ require (
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
- github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.6 // indirect
@@ -100,7 +101,6 @@ require (
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.6-0.20200504143853-81378bbcd8a1 // indirect
- github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
diff --git a/go.sum b/go.sum
index 217f9ee8..7fc8d5cb 100644
--- a/go.sum
+++ b/go.sum
@@ -70,9 +70,11 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@@ -89,7 +91,6 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
@@ -525,8 +526,6 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
-github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
diff --git a/helpers/e2ehelper/data_flow_tester.go b/helpers/e2ehelper/data_flow_tester.go
index 2474b046..28c33875 100644
--- a/helpers/e2ehelper/data_flow_tester.go
+++ b/helpers/e2ehelper/data_flow_tester.go
@@ -38,6 +38,7 @@ import (
"os"
"strconv"
"strings"
+ "sync"
"testing"
"time"
)
@@ -73,6 +74,18 @@ type DataFlowTester struct {
Log core.Logger
}
+type TableOptions struct {
+ // CSVRelPath relative path to the CSV file that contains the seeded data
+ CSVRelPath string
+ // TargetFields the fields (columns) to consider for verification. Leave empty to default to all.
+ TargetFields []string
+ // IgnoreFields the fields (columns) to ignore/skip.
+ IgnoreFields []string
+ // IgnoreTypes similar to IgnoreFields, this will ignore the fields contained in the type. Useful for ignoring embedded
+ // types and their fields in the target model
+ IgnoreTypes []interface{}
+}
+
// NewDataFlowTester create a *DataFlowTester to help developer test their subtasks data flow
func NewDataFlowTester(t *testing.T, pluginName string, pluginMeta core.PluginMeta) *DataFlowTester {
err := core.RegisterPlugin(pluginName, pluginMeta)
@@ -174,24 +187,51 @@ func (t *DataFlowTester) Subtask(subtaskMeta core.SubTaskMeta, taskData interfac
}
func (t *DataFlowTester) getPkFields(dst schema.Tabler) []string {
+ return t.getFields(dst, func(column gorm.ColumnType) bool {
+ isPk, _ := column.PrimaryKey()
+ return isPk
+ })
+}
+
+func filterColumn(column gorm.ColumnType, opts TableOptions) bool {
+ for _, ignore := range opts.IgnoreFields {
+ if column.Name() == ignore {
+ return false
+ }
+ }
+ if len(opts.TargetFields) == 0 {
+ return true
+ }
+ targetFound := false
+ for _, target := range opts.TargetFields {
+ if column.Name() == target {
+ targetFound = true
+ break
+ }
+ }
+ return targetFound
+}
+
+func (t *DataFlowTester) getFields(dst schema.Tabler, filter func(column gorm.ColumnType) bool) []string {
columnTypes, err := t.Db.Migrator().ColumnTypes(dst)
- var pkFields []string
+ var fields []string
if err != nil {
panic(err)
}
for _, columnType := range columnTypes {
- if isPrimaryKey, _ := columnType.PrimaryKey(); isPrimaryKey {
- pkFields = append(pkFields, columnType.Name())
+ if filter == nil || filter(columnType) {
+ fields = append(fields, columnType.Name())
}
}
- return pkFields
+ return fields
}
// CreateSnapshot reads rows from database and write them into .csv file.
-func (t *DataFlowTester) CreateSnapshot(dst schema.Tabler, csvRelPath string, targetfields []string) {
+func (t *DataFlowTester) CreateSnapshot(dst schema.Tabler, opts TableOptions) {
location, _ := time.LoadLocation(`UTC`)
pkFields := t.getPkFields(dst)
- allFields := append(pkFields, targetfields...)
+ targetFields := t.resolveTargetFields(dst, opts)
+ allFields := append(pkFields, targetFields...)
allFields = utils.StringsUniq(allFields)
dbCursor, err := t.Dal.Cursor(
dal.Select(strings.Join(allFields, `,`)),
@@ -199,14 +239,14 @@ func (t *DataFlowTester) CreateSnapshot(dst schema.Tabler, csvRelPath string, ta
dal.Orderby(strings.Join(pkFields, `,`)),
)
if err != nil {
- panic(err)
+ panic(fmt.Errorf("unable to run select query on table %s: %v", dst.TableName(), err))
}
columns, err := dbCursor.Columns()
if err != nil {
- panic(err)
+ panic(fmt.Errorf("unable to get columns from table %s: %v", dst.TableName(), err))
}
- csvWriter := pluginhelper.NewCsvFileWriter(csvRelPath, columns)
+ csvWriter := pluginhelper.NewCsvFileWriter(opts.CSVRelPath, columns)
defer csvWriter.Close()
// define how to scan value
@@ -225,7 +265,7 @@ func (t *DataFlowTester) CreateSnapshot(dst schema.Tabler, csvRelPath string, ta
for dbCursor.Next() {
err = dbCursor.Scan(forScanValues...)
if err != nil {
- panic(err)
+ panic(fmt.Errorf("unable to scan row on table %s: %v", dst.TableName(), err))
}
values := make([]string, len(allFields))
for i := range forScanValues {
@@ -249,6 +289,7 @@ func (t *DataFlowTester) CreateSnapshot(dst schema.Tabler, csvRelPath string, ta
}
csvWriter.Write(values)
}
+ fmt.Printf("created CSV file: %s\n", opts.CSVRelPath)
}
// ExportRawTable reads rows from raw table and write them into .csv file.
@@ -300,20 +341,57 @@ func formatDbValue(value interface{}) string {
return ``
}
-// VerifyTable reads rows from csv file and compare with records from database one by one. You must specified the
+// VerifyTable reads rows from csv file and compare with records from database one by one. You must specify the
// Primary Key Fields with `pkFields` so DataFlowTester could select the exact record from database, as well as which
-// fields to compare with by specifying `targetFields` parameter.
+// fields to compare with by specifying `targetFields` parameter. Leaving `targetFields` empty/nil will compare all fields.
func (t *DataFlowTester) VerifyTable(dst schema.Tabler, csvRelPath string, targetFields []string) {
- _, err := os.Stat(csvRelPath)
+ t.VerifyTableWithOptions(dst, TableOptions{
+ CSVRelPath: csvRelPath,
+ TargetFields: targetFields,
+ })
+}
+
+func (t *DataFlowTester) extractColumns(ifc interface{}) []string {
+ sch, err := schema.Parse(ifc, &sync.Map{}, schema.NamingStrategy{})
+ if err != nil {
+ panic(fmt.Sprintf("error getting object schema: %v", err))
+ }
+ var columns []string
+ for _, f := range sch.Fields {
+ columns = append(columns, f.DBName)
+ }
+ return columns
+}
+
+func (t *DataFlowTester) resolveTargetFields(dst schema.Tabler, opts TableOptions) []string {
+ for _, ignore := range opts.IgnoreTypes {
+ opts.IgnoreFields = append(opts.IgnoreFields, t.extractColumns(ignore)...)
+ }
+ var targetFields []string
+ if len(opts.TargetFields) == 0 || len(opts.IgnoreFields) > 0 {
+ targetFields = append(targetFields, t.getFields(dst, func(column gorm.ColumnType) bool {
+ return filterColumn(column, opts)
+ })...)
+ } else {
+ targetFields = opts.TargetFields
+ }
+ return targetFields
+}
+
+// VerifyTableWithOptions extends VerifyTable and allows for more advanced usages using TableOptions
+func (t *DataFlowTester) VerifyTableWithOptions(dst schema.Tabler, opts TableOptions) {
+ if opts.CSVRelPath == "" {
+ panic("CSV relative path missing")
+ }
+ _, err := os.Stat(opts.CSVRelPath)
if os.IsNotExist(err) {
- t.CreateSnapshot(dst, csvRelPath, targetFields)
+ t.CreateSnapshot(dst, opts)
return
}
+ targetFields := t.resolveTargetFields(dst, opts)
pkFields := t.getPkFields(dst)
-
- csvIter := pluginhelper.NewCsvFileIterator(csvRelPath)
+ csvIter := pluginhelper.NewCsvFileIterator(opts.CSVRelPath)
defer csvIter.Close()
-
expectedTotal := int64(0)
csvMap := map[string]map[string]interface{}{}
for csvIter.HasNext() {
@@ -325,9 +403,11 @@ func (t *DataFlowTester) VerifyTable(dst schema.Tabler, csvRelPath string, targe
pkValueStr := strings.Join(pkValues, `-`)
_, ok := csvMap[pkValueStr]
assert.False(t.T, ok, fmt.Sprintf(`%s duplicated in csv (with params from csv %s)`, dst.TableName(), pkValues))
+ for _, ignore := range opts.IgnoreFields {
+ delete(expected, ignore)
+ }
csvMap[pkValueStr] = expected
}
-
dbRows := &[]map[string]interface{}{}
err = t.Db.Table(dst.TableName()).Find(dbRows).Error
if err != nil {
@@ -348,7 +428,6 @@ func (t *DataFlowTester) VerifyTable(dst schema.Tabler, csvRelPath string, targe
}
expectedTotal++
}
-
var actualTotal int64
err = t.Db.Table(dst.TableName()).Count(&actualTotal).Error
if err != nil {
diff --git a/helpers/e2ehelper/data_flow_tester_test.go b/helpers/e2ehelper/data_flow_tester_test.go
index 8c0b5c8d..e98d792f 100644
--- a/helpers/e2ehelper/data_flow_tester_test.go
+++ b/helpers/e2ehelper/data_flow_tester_test.go
@@ -18,13 +18,27 @@ limitations under the License.
package e2ehelper
import (
+ "github.com/apache/incubator-devlake/models/common"
gitlabModels "github.com/apache/incubator-devlake/plugins/gitlab/models"
+ "github.com/stretchr/testify/assert"
+ "gorm.io/gorm"
"testing"
"github.com/apache/incubator-devlake/plugins/core"
"github.com/apache/incubator-devlake/plugins/gitlab/tasks"
)
+type TestModel struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ IssueId int `gorm:"primaryKey;autoIncrement:false"`
+ LabelName string `gorm:"primaryKey;type:varchar(255)"`
+ common.NoPKModel
+}
+
+func (t TestModel) TableName() string {
+ return "_tool_test_model"
+}
+
func ExampleDataFlowTester() {
var t *testing.T // stub
@@ -68,3 +82,142 @@ func ExampleDataFlowTester() {
},
)
}
+
+func TestGetTableMetaData(t *testing.T) {
+ var meta core.PluginMeta
+ dataflowTester := NewDataFlowTester(t, "test_dataflow", meta)
+ dataflowTester.FlushTabler(&TestModel{})
+ t.Run("get_fields", func(t *testing.T) {
+ fields := dataflowTester.getFields(&TestModel{}, func(column gorm.ColumnType) bool {
+ return true
+ })
+ assert.Equal(t, 9, len(fields))
+ for _, e := range []string{
+ "connection_id",
+ "issue_id",
+ "label_name",
+ "created_at",
+ "updated_at",
+ "_raw_data_params",
+ "_raw_data_table",
+ "_raw_data_id",
+ "_raw_data_remark",
+ } {
+ assert.Contains(t, fields, e)
+ }
+ })
+ t.Run("extract_columns", func(t *testing.T) {
+ columns := dataflowTester.extractColumns(&common.RawDataOrigin{})
+ assert.Equal(t, 4, len(columns))
+ for _, e := range []string{
+ "_raw_data_params",
+ "_raw_data_table",
+ "_raw_data_id",
+ "_raw_data_remark",
+ } {
+ assert.Contains(t, columns, e)
+ }
+ })
+ t.Run("get_pk_fields", func(t *testing.T) {
+ fields := dataflowTester.getPkFields(&TestModel{})
+ assert.Equal(t, 3, len(fields))
+ for _, e := range []string{
+ "connection_id",
+ "issue_id",
+ "label_name",
+ } {
+ assert.Contains(t, fields, e)
+ }
+ })
+ t.Run("resolve_fields_targetFieldsOnly", func(t *testing.T) {
+ fields := dataflowTester.resolveTargetFields(&TestModel{}, TableOptions{
+ TargetFields: []string{"connection_id"},
+ IgnoreFields: nil,
+ IgnoreTypes: nil,
+ })
+ assert.Equal(t, 1, len(fields))
+ for _, e := range []string{"connection_id"} {
+ assert.Contains(t, fields, e)
+ }
+ })
+ t.Run("resolve_fields_ignoreFieldsOnly", func(t *testing.T) {
+ fields := dataflowTester.resolveTargetFields(&TestModel{}, TableOptions{
+ TargetFields: nil,
+ IgnoreFields: []string{
+ "label_name",
+ "created_at",
+ "updated_at",
+ "_raw_data_params",
+ "_raw_data_table",
+ "_raw_data_id",
+ "_raw_data_remark",
+ },
+ IgnoreTypes: nil,
+ })
+ assert.Equal(t, 2, len(fields))
+ for _, e := range []string{"connection_id", "issue_id"} {
+ assert.Contains(t, fields, e)
+ }
+ })
+ t.Run("resolve_fields_ignoreFieldsOnly", func(t *testing.T) {
+ fields := dataflowTester.resolveTargetFields(&TestModel{}, TableOptions{
+ TargetFields: nil,
+ IgnoreFields: []string{
+ "label_name",
+ "created_at",
+ "updated_at",
+ "_raw_data_params",
+ "_raw_data_table",
+ "_raw_data_id",
+ "_raw_data_remark",
+ },
+ IgnoreTypes: nil,
+ })
+ assert.Equal(t, 2, len(fields))
+ for _, e := range []string{"connection_id", "issue_id"} {
+ assert.Contains(t, fields, e)
+ }
+ })
+ t.Run("resolve_fields_ignoreType", func(t *testing.T) {
+ fields := dataflowTester.resolveTargetFields(&TestModel{}, TableOptions{
+ TargetFields: nil,
+ IgnoreFields: nil,
+ IgnoreTypes: []interface{}{&common.NoPKModel{}},
+ })
+ assert.Equal(t, 3, len(fields))
+ for _, e := range []string{
+ "connection_id",
+ "issue_id",
+ "label_name",
+ } {
+ assert.Contains(t, fields, e)
+ }
+ })
+ t.Run("resolve_fields_ignoreType_ignoreFields", func(t *testing.T) {
+ fields := dataflowTester.resolveTargetFields(&TestModel{}, TableOptions{
+ TargetFields: nil,
+ IgnoreFields: []string{"label_name"},
+ IgnoreTypes: []interface{}{&common.NoPKModel{}},
+ })
+ assert.Equal(t, 2, len(fields))
+ for _, e := range []string{
+ "connection_id",
+ "issue_id",
+ } {
+ assert.Contains(t, fields, e)
+ }
+ })
+ t.Run("resolve_fields_targetFields_ignoreType_ignoreFields", func(t *testing.T) {
+ fields := dataflowTester.resolveTargetFields(&TestModel{}, TableOptions{
+ TargetFields: []string{"label_name", "createdAt", "connection_id"},
+ IgnoreFields: []string{"label_name"},
+ IgnoreTypes: []interface{}{&common.NoPKModel{}},
+ })
+ assert.Equal(t, 1, len(fields))
+ for _, e := range []string{
+ "connection_id",
+ } {
+ assert.Contains(t, fields, e)
+ }
+ })
+}
diff --git a/plugins/github/e2e/issue_test.go b/plugins/github/e2e/issue_test.go
index 1b33d02c..3cbec5cb 100644
--- a/plugins/github/e2e/issue_test.go
+++ b/plugins/github/e2e/issue_test.go
@@ -81,6 +81,7 @@ func TestIssueDataFlow(t *testing.T) {
"author_name",
"assignee_id",
"assignee_name",
+ "milestone_id",
"lead_time_minutes",
"url",
"closed_at",
diff --git a/plugins/github/e2e/milestone_test.go b/plugins/github/e2e/milestone_test.go
new file mode 100644
index 00000000..465f3c8f
--- /dev/null
+++ b/plugins/github/e2e/milestone_test.go
@@ -0,0 +1,76 @@
+package e2e
+
+import (
+ "github.com/apache/incubator-devlake/helpers/e2ehelper"
+ "github.com/apache/incubator-devlake/models/common"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "github.com/apache/incubator-devlake/plugins/github/impl"
+ "github.com/apache/incubator-devlake/plugins/github/models"
+ "github.com/apache/incubator-devlake/plugins/github/tasks"
+ "testing"
+ "time"
+)
+
+func TestMilestoneDataFlow(t *testing.T) {
+ var plugin impl.Github
+ dataflowTester := e2ehelper.NewDataFlowTester(t, "github", plugin)
+ githubRepository := &models.GithubRepo{
+ ConnectionId: 1,
+ GithubId: 134018330,
+ CreatedDate: func() time.Time {
+ createdTime, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
+ return createdTime
+ }(),
+ }
+ taskData := &tasks.GithubTaskData{
+ Options: &tasks.GithubOptions{
+ ConnectionId: 1,
+ Owner: "panjf2000",
+ Repo: "ants",
+ TransformationRules: models.TransformationRules{
+ PrType: "type/(.*)$",
+ PrComponent: "component/(.*)$",
+ PrBodyClosePattern: "(?mi)(fix|close|resolve|fixes|closes|resolves|fixed|closed|resolved)[\\s]*.*(((and )?(#|https:\\/\\/github.com\\/%s\\/%s\\/issues\\/)\\d+[ ]*)+)",
+ IssueSeverity: "severity/(.*)$",
+ IssuePriority: "^(highest|high|medium|low)$",
+ IssueComponent: "component/(.*)$",
+ IssueTypeBug: "^(bug|failure|error)$",
+ IssueTypeIncident: "",
+ IssueTypeRequirement: "^(feat|feature|proposal|requirement)$",
+ },
+ },
+ Repo: githubRepository,
+ }
+
+ dataflowTester.FlushTabler(&models.GithubMilestone{})
+ dataflowTester.FlushTabler(&models.GithubIssue{})
+
+ // import raw data table
+ dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_github_api_milestones.csv", "_raw_"+tasks.RAW_MILESTONE_TABLE)
+ dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_github_api_issues.csv", "_raw_"+tasks.RAW_ISSUE_TABLE)
+
+ dataflowTester.Subtask(tasks.ExtractApiIssuesMeta, taskData)
+ dataflowTester.Subtask(tasks.ExtractMilestonesMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&models.GithubMilestone{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/_tool_github_milestones.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+
+ dataflowTester.FlushTabler(&ticket.Sprint{})
+ dataflowTester.FlushTabler(&ticket.BoardSprint{})
+ dataflowTester.FlushTabler(&ticket.SprintIssue{})
+
+ dataflowTester.Subtask(tasks.ConvertMilestonesMeta, taskData)
+ dataflowTester.VerifyTableWithOptions(&ticket.Sprint{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/sprints.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+ dataflowTester.VerifyTableWithOptions(&ticket.BoardSprint{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/board_sprint.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+ dataflowTester.VerifyTableWithOptions(&ticket.SprintIssue{}, e2ehelper.TableOptions{
+ CSVRelPath: "./snapshot_tables/sprint_issue.csv",
+ IgnoreTypes: []interface{}{common.NoPKModel{}},
+ })
+}
diff --git a/plugins/github/e2e/raw_tables/_raw_github_api_issues.csv b/plugins/github/e2e/raw_tables/_raw_github_api_issues.csv
index 1c8760f3..00b79cd6 100644
--- a/plugins/github/e2e/raw_tables/_raw_github_api_issues.csv
+++ b/plugins/github/e2e/raw_tables/_raw_github_api_issues.csv
@@ -1,43 +1,43 @@
id,params,data,url,input,created_at
-9,"{""ConnectionId"":2,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/4"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/events"",""html_url"":""https://github.com/panjf2000 [...]
-10,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/4"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/events"",""html_url"":""https://github.com/panjf200 [...]
-11,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/5"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/5/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/5/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/5/events"",""html_url"":""https://github.com/panjf200 [...]
-12,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/6"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/6/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/6/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/6/events"",""html_url"":""https://github.com/panjf200 [...]
-13,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/7"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/7/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/7/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/7/events"",""html_url"":""https://github.com/panjf200 [...]
-14,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/8"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/8/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/8/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/8/events"",""html_url"":""https://github.com/panjf200 [...]
-15,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/9"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/9/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/9/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/9/events"",""html_url"":""https://github.com/panjf200 [...]
-16,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/10"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/10/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/10/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/10/events"",""html_url"":""https://github.com/panj [...]
-17,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/11"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/11/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/11/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/11/events"",""html_url"":""https://github.com/panj [...]
-18,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/12"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/12/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/12/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/12/events"",""html_url"":""https://github.com/panj [...]
-19,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/13"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/13/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/13/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/13/events"",""html_url"":""https://github.com/panj [...]
-20,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/14"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/14/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/14/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/14/events"",""html_url"":""https://github.com/panj [...]
-21,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/15"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/15/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/15/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/15/events"",""html_url"":""https://github.com/panj [...]
-22,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/16"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/16/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/16/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/16/events"",""html_url"":""https://github.com/panj [...]
-23,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/17"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/17/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/17/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/17/events"",""html_url"":""https://github.com/panj [...]
-24,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/18"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/18/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/18/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/18/events"",""html_url"":""https://github.com/panj [...]
-25,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/19"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/19/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/19/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/19/events"",""html_url"":""https://github.com/panj [...]
-26,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/20"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/20/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/20/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/20/events"",""html_url"":""https://github.com/panj [...]
-27,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/21"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/21/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/21/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/21/events"",""html_url"":""https://github.com/panj [...]
-28,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/22"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/22/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/22/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/22/events"",""html_url"":""https://github.com/panj [...]
-29,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/23"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/23/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/23/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/23/events"",""html_url"":""https://github.com/panj [...]
-30,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/24"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/24/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/24/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/24/events"",""html_url"":""https://github.com/panj [...]
-31,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/25"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/25/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/25/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/25/events"",""html_url"":""https://github.com/panj [...]
-32,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/26"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/26/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/26/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/26/events"",""html_url"":""https://github.com/panj [...]
-33,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/27"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/27/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/27/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/27/events"",""html_url"":""https://github.com/panj [...]
-34,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/28"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/28/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/28/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/28/events"",""html_url"":""https://github.com/panj [...]
-35,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/29"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/29/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/29/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/29/events"",""html_url"":""https://github.com/panj [...]
-36,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/30"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/30/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/30/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/30/events"",""html_url"":""https://github.com/panj [...]
-37,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/31"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/31/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/31/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/31/events"",""html_url"":""https://github.com/panj [...]
-38,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/32"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/32/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/32/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/32/events"",""html_url"":""https://github.com/panj [...]
-39,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/33"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/33/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/33/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/33/events"",""html_url"":""https://github.com/panj [...]
-40,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/34"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/34/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/34/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/34/events"",""html_url"":""https://github.com/panj [...]
-41,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/35"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/35/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/35/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/35/events"",""html_url"":""https://github.com/panj [...]
-42,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/36"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/36/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/36/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/36/events"",""html_url"":""https://github.com/panj [...]
-43,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/37"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/37/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/37/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/37/events"",""html_url"":""https://github.com/panj [...]
-44,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/38"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/38/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/38/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/38/events"",""html_url"":""https://github.com/panj [...]
-45,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/39"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/39/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/39/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/39/events"",""html_url"":""https://github.com/panj [...]
-46,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/40"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/40/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/40/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/40/events"",""html_url"":""https://github.com/panj [...]
-47,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/41"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/41/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/41/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/41/events"",""html_url"":""https://github.com/panj [...]
-48,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/42"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/42/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/42/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/42/events"",""html_url"":""https://github.com/panj [...]
-49,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/43"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/43/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/43/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/43/events"",""html_url"":""https://github.com/panj [...]
-50,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/44"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/44/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/44/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/44/events"",""html_url"":""https://github.com/panj [...]
+9,"{""ConnectionId"":2,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/4"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/events"",""html_url"":""https://github.com/panjf2000 [...]
+10,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/4"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/4/events"",""html_url"":""https://github.com/panjf200 [...]
+11,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/5"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/5/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/5/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/5/events"",""html_url"":""https://github.com/panjf200 [...]
+12,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/6"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/6/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/6/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/6/events"",""html_url"":""https://github.com/panjf200 [...]
+13,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/7"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/7/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/7/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/7/events"",""html_url"":""https://github.com/panjf200 [...]
+14,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/8"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/8/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/8/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/8/events"",""html_url"":""https://github.com/panjf200 [...]
+15,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/9"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/9/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/9/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/9/events"",""html_url"":""https://github.com/panjf200 [...]
+16,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/10"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/10/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/10/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/10/events"",""html_url"":""https://github.com/panj [...]
+17,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/11"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/11/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/11/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/11/events"",""html_url"":""https://github.com/panj [...]
+18,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/12"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/12/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/12/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/12/events"",""html_url"":""https://github.com/panj [...]
+19,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/13"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/13/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/13/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/13/events"",""html_url"":""https://github.com/panj [...]
+20,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/14"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/14/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/14/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/14/events"",""html_url"":""https://github.com/panj [...]
+21,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/15"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/15/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/15/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/15/events"",""html_url"":""https://github.com/panj [...]
+22,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/16"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/16/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/16/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/16/events"",""html_url"":""https://github.com/panj [...]
+23,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/17"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/17/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/17/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/17/events"",""html_url"":""https://github.com/panj [...]
+24,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/18"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/18/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/18/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/18/events"",""html_url"":""https://github.com/panj [...]
+25,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/19"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/19/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/19/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/19/events"",""html_url"":""https://github.com/panj [...]
+26,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/20"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/20/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/20/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/20/events"",""html_url"":""https://github.com/panj [...]
+27,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/21"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/21/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/21/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/21/events"",""html_url"":""https://github.com/panj [...]
+28,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/22"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/22/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/22/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/22/events"",""html_url"":""https://github.com/panj [...]
+29,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/23"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/23/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/23/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/23/events"",""html_url"":""https://github.com/panj [...]
+30,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/24"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/24/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/24/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/24/events"",""html_url"":""https://github.com/panj [...]
+31,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/25"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/25/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/25/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/25/events"",""html_url"":""https://github.com/panj [...]
+32,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/26"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/26/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/26/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/26/events"",""html_url"":""https://github.com/panj [...]
+33,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/27"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/27/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/27/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/27/events"",""html_url"":""https://github.com/panj [...]
+34,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/28"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/28/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/28/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/28/events"",""html_url"":""https://github.com/panj [...]
+35,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/29"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/29/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/29/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/29/events"",""html_url"":""https://github.com/panj [...]
+36,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/30"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/30/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/30/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/30/events"",""html_url"":""https://github.com/panj [...]
+37,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/31"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/31/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/31/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/31/events"",""html_url"":""https://github.com/panj [...]
+38,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/32"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/32/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/32/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/32/events"",""html_url"":""https://github.com/panj [...]
+39,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/33"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/33/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/33/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/33/events"",""html_url"":""https://github.com/panj [...]
+40,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/34"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/34/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/34/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/34/events"",""html_url"":""https://github.com/panj [...]
+41,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/35"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/35/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/35/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/35/events"",""html_url"":""https://github.com/panj [...]
+42,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/36"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/36/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/36/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/36/events"",""html_url"":""https://github.com/panj [...]
+43,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/37"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/37/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/37/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/37/events"",""html_url"":""https://github.com/panj [...]
+44,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/38"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/38/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/38/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/38/events"",""html_url"":""https://github.com/panj [...]
+45,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/39"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/39/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/39/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/39/events"",""html_url"":""https://github.com/panj [...]
+46,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/40"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/40/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/40/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/40/events"",""html_url"":""https://github.com/panj [...]
+47,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/41"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/41/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/41/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/41/events"",""html_url"":""https://github.com/panj [...]
+48,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/42"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/42/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/42/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/42/events"",""html_url"":""https://github.com/panj [...]
+49,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/43"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/43/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/43/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/43/events"",""html_url"":""https://github.com/panj [...]
+50,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/panjf2000/ants/issues/44"",""repository_url"":""https://api.github.com/repos/panjf2000/ants"",""labels_url"":""https://api.github.com/repos/panjf2000/ants/issues/44/labels{/name}"",""comments_url"":""https://api.github.com/repos/panjf2000/ants/issues/44/comments"",""events_url"":""https://api.github.com/repos/panjf2000/ants/issues/44/events"",""html_url"":""https://github.com/panj [...]
diff --git a/plugins/github/e2e/raw_tables/_raw_github_api_milestones.csv b/plugins/github/e2e/raw_tables/_raw_github_api_milestones.csv
new file mode 100644
index 00000000..32cacaad
--- /dev/null
+++ b/plugins/github/e2e/raw_tables/_raw_github_api_milestones.csv
@@ -0,0 +1,2 @@
+id,params,data,url,input,created_at
+109,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}","{""url"":""https://api.github.com/repos/apache/incubator-devlake/milestones/7"",""html_url"":""https://github.com/apache/incubator-devlake/milestone/7"",""labels_url"":""https://api.github.com/repos/apache/incubator-devlake/milestones/7/labels"",""id"":7856149,""node_id"":""MI_kwDOFuUSzs4Ad-AV"",""number"":7,""title"":""v0.11.0"",""description"":null,""creator"":{""login"":""Startrekzky"",""id"":14050754,""node_id"":"" [...]
diff --git a/plugins/github/e2e/snapshot_tables/_tool_github_issues.csv b/plugins/github/e2e/snapshot_tables/_tool_github_issues.csv
index 801fc2dd..071ad042 100644
--- a/plugins/github/e2e/snapshot_tables/_tool_github_issues.csv
+++ b/plugins/github/e2e/snapshot_tables/_tool_github_issues.csv
@@ -1,27 +1,27 @@
-connection_id,github_id,repo_id,number,state,title,body,priority,type,status,author_id,author_name,assignee_id,assignee_name,lead_time_minutes,url,closed_at,github_created_at,github_updated_at,severity,component,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark
-1,346842831,134018330,5,closed,关于 <-p.freeSignal 的疑惑,"""Hi,\r\n 我阅读了源码,对 `<-p.freeSignal` 这句代码有疑惑。 这句代码出现在了多个地方,freeSignal 的作用英文注释我是理解的,并且知道在 `putWorker` 中才进行 `p.freeSignal <- sig{}`\r\n\r\n对于下面的代码\r\n```\r\nfunc (p *Pool) getWorker() *Worker {\r\n\tvar w *Worker\r\n\twaiting := false\r\n\r\n\tp.lock.Lock()\r\n\tidleWorkers := p.workers\r\n\tn := len(idleWorkers) - 1\r\n\tif n < 0 { // 说明 pool中没有worker了\r\n\t\twaiting = p.Running() >= p.Cap()\r\n\t} else { // 说明pool中有worker\r\n\t\t<-p [...]
-1,347255859,134018330,6,closed,死锁bug,"""func (p *Pool) getWorker() *Worker 这个函数的 199行 \r\n必须先解锁在加锁, 要不然会产生死锁\r\n\r\n\tp.lock.Unlock()\r\n\t\t<-p.freeSignal\r\n\t\tp.lock.Lock()""",,BUG,,13118848,lovelly,0,,1786,https://github.com/panjf2000/ants/issues/6,2018-08-04T10:18:41.000+00:00,2018-08-03T04:32:28.000+00:00,2018-08-04T10:18:41.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,12,
-1,348630179,134018330,7,closed,清理过期协程报错,"""你好,非常感谢提供这么好用的工具包。我在使用ants时,发现报异常。结果见下图\r\n![image](https://user-images.githubusercontent.com/4555057/43823431-98384444-9b21-11e8-880c-7458b931734a.png)\r\n日志是我在periodicallyPurge里加的调试信息\r\n![image](https://user-images.githubusercontent.com/4555057/43823534-e3c624a8-9b21-11e8-96c6-512e3e08db22.png)\r\n\r\n### 原因分析\r\n\r\n我认为可能原因是没有处理n==0的情况\r\n```\r\nif n > 0 {\r\n\tn++\r\n\tp.workers = idleWorkers[n:]\r\n}\r\n```\r\n\r\n\r\n### 测试代码\r\n```\r\npa [...]
-1,356703393,134018330,10,closed,高并发下设定较小的worker数量问题,"""会存在cpu飚升的问题吧?""",,,,11763614,Moonlight-Zhao,0,,36198,https://github.com/panjf2000/ants/issues/10,2018-09-29T11:45:00.000+00:00,2018-09-04T08:26:55.000+00:00,2018-09-29T11:45:00.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,16,
-1,364361014,134018330,12,closed,潘少,更新下tag吧,"""鄙人现在在弄dep依赖管理,有用到你写的ants项目,可是你好像忘记打最新的tag了。最新的tag 3.6是指向ed55924这个提交,git上的最新代码是af376f1b这次提交,两次提交都隔了快5个月了,看到的话,麻烦打一个最新的tag吧。(手动可怜)""",,,,29452204,edcismybrother,0,,1293,https://github.com/panjf2000/ants/issues/12,2018-09-28T06:05:58.000+00:00,2018-09-27T08:32:25.000+00:00,2019-04-21T08:19:58.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,18,
-1,381941219,134018330,17,closed,关于优雅退出的问题,"""关于这个package优雅退出的问题,我看了一下Release的代码:\r\n\r\n`\r\n\t// Release Closed this pool.\r\n\tfunc (p *PoolWithFunc) Release() error {\r\n\t\tp.once.Do(func() {\r\n\t\t\tp.release <- sig{}\r\n\t\t\tp.lock.Lock()\r\n\t\t\tidleWorkers := p.workers\r\n\t\t\tfor i, w := range idleWorkers {\r\n\t\t\t\tw.args <- nil\r\n\t\t\t\tidleWorkers[i] = nil\r\n\t\t\t}\r\n\t\t\tp.workers = nil\r\n\t\t\tp.lock.Unlock()\r\n\t\t})\r\n\t\treturn nil\r\n\t}\r\n`\r\n\r\nrelea [...]
-1,382039050,134018330,18,closed,go协程的理解,"""你好楼主,向您请教一个协程和线程的问题,协程基于go进程调度,线程基于系统内核调度,调度协程的过程是先调度线程后获得资源再去调度协程。\""官方解释: GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously。限制cpu数,本质上是什么,限制并行数?,并行数即同时执行数量?,执行单元即线程?,即限制最大并行线程数量?\""""",,,,13944100,LinuxForYQH,0,,20213,https://github.com/panjf2000/ants/issues/18,2018-12-03T03:53:50.000+00:00,2018-11-19T02:59:53.000+00:00,2018-12-03T03:53:50.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""} [...]
-1,382574800,134018330,20,closed,是否考虑任务支持回调函数处理失败的逻辑和任务依赖,"""#""",,,,5668717,kklinan,0,,95398,https://github.com/panjf2000/ants/issues/20,2019-01-25T15:34:03.000+00:00,2018-11-20T09:36:02.000+00:00,2019-01-25T15:34:03.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,26,
-1,388907811,134018330,21,closed,Benchmark 下直接使用 Semaphore 似乎更快呢?,"""简单跑了一下 benchmark,Semaphore 更快且很简单\r\n\r\n```bash\r\n$ go test -bench .\r\ngoos: darwin\r\ngoarch: amd64\r\npkg: github.com/panjf2000/ants\r\nBenchmarkGoroutineWithFunc-4 \t 1\t3445631705 ns/op\r\nBenchmarkSemaphoreWithFunc-4 \t 1\t1037219073 ns/op\r\nBenchmarkAntsPoolWithFunc-4 \t 1\t1138053222 ns/op\r\nBenchmarkGoroutine-4 \t 2\t 731850771 ns/op\r\nBenchmarkSemaphore-4 [...]
-1,401277739,134018330,22,closed,是否考虑 worker 中添加 PanicHandler ?,"""比方说在创建 Pool 的时候传入一个 PanicHandler,然后在每个 worker 创建的时候 recover 之后传给 PanicHandler 处理。否则池子里如果发生 panic 会直接挂掉整个进程。""",,,,8923413,choleraehyq,0,,1174,https://github.com/panjf2000/ants/issues/22,2019-01-22T05:41:34.000+00:00,2019-01-21T10:06:56.000+00:00,2019-01-22T05:41:34.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,28,
-1,402513849,134018330,24,closed,提交任务不阻塞,"""`Pool.Submit`和`PoolWithFunc.Server`提交任务,如果没有空的worker,会一直阻塞。建议增加不阻塞的接口,当前失败时直接返回错误。""",,,,5044825,tenfyzhong,0,,300032,https://github.com/panjf2000/ants/issues/24,2019-08-20T10:56:30.000+00:00,2019-01-24T02:24:13.000+00:00,2019-08-20T10:56:30.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,30,
-1,405951301,134018330,25,closed,use example errors,"""./antstest.go:37:14: cannot use syncCalculateSum (type func()) as type ants.f in argument to ants.Submit\r\n./antstest.go:45:35: cannot use func literal (type func(interface {})) as type ants.pf in argument to ants.NewPoolWithFunc\r\n""",,,,5244267,jiashiwen,0,,3088,https://github.com/panjf2000/ants/issues/25,2019-02-04T09:11:52.000+00:00,2019-02-02T05:43:38.000+00:00,2019-02-04T09:11:52.000+00:00,,,"{""ConnectionId"":1,""Owner"":""pa [...]
-1,413968505,134018330,26,closed,running可能大于cap的问题,"""running与cap的比较判断与incRuning分开执行的, 可能会出现running大于cap的问题?\r\n`func (p *Pool) retrieveWorker() *Worker {\r\n\tvar w *Worker\r\n\r\n\tp.lock.Lock()\r\n\tidleWorkers := p.workers\r\n\tn := len(idleWorkers) - 1\r\n\tif n >= 0 {\r\n\t\tw = idleWorkers[n]\r\n\t\tidleWorkers[n] = nil\r\n\t\tp.workers = idleWorkers[:n]\r\n\t\tp.lock.Unlock()\r\n\t} else if p.Running() < p.Cap() {\r\n\t\tp.lock.Unlock()\r\n\t\tif cacheWorker := p.workerCache.Get() [...]
-1,419183961,134018330,27,closed,为何goroutine一直上不去,用户量也打不上去,"""为何goroutine一直上不去,用户量也打不上去\r\n是我用的有问题吗?\r\n\r\nwebsocket server\r\nhttps://github.com/im-ai/pushm/blob/master/learn/goroutine/goroutinepoolwebsocket.go\r\n\r\nwebsocket cient\r\nhttps://github.com/im-ai/pushm/blob/master/learn/goroutine/goroutinepoolwebsocketclient.go\r\n""",,,,38367404,liliang8858,0,,37496,https://github.com/panjf2000/ants/issues/27,2019-04-05T14:05:20.000+00:00,2019-03-10T13:08:52.000+00:00,2019-04-05T14:05:20 [...]
-1,419268851,134018330,28,closed,cap 和 running 比较的问题,"""这是我在 Playground 上面的代码 https://play.golang.org/p/D94YUU3FnX6\r\natomic 只能保证自增自减时的原子操作,在比较过程中,其他线程对变量进行了操作 比较过程并无感知,所以这个比较结果 不是完全正确的,想要实现 比较的数量完全正确,只能在修改和比较两个值的地方加锁\r\n像 #26 说的是对的""",,,,29243953,naiba,0,,237002,https://github.com/panjf2000/ants/issues/28,2019-08-22T16:27:37.000+00:00,2019-03-11T02:24:41.000+00:00,2019-08-22T16:27:37.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,34,
-1,424634533,134018330,29,closed,任务传参,"""你好,你的项目太酷了👍\r\n\r\nhttps://github.com/panjf2000/ants/blob/master/pool.go#L124 貌似不支持带参数的任务, 请问传参是用闭包的方式吗?\r\n""",,,,8509898,prprprus,0,,999,https://github.com/panjf2000/ants/issues/29,2019-03-25T09:32:11.000+00:00,2019-03-24T16:52:21.000+00:00,2019-03-25T09:45:05.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,35,
-1,429972115,134018330,31,closed,Add go.mod,"""""",,,,48135919,tsatke,0,,3474,https://github.com/panjf2000/ants/issues/31,2019-04-08T09:45:31.000+00:00,2019-04-05T23:50:36.000+00:00,2019-10-17T03:12:19.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,37,
-1,433564955,134018330,32,closed,关于版本问题,我发现小版本(0.0.x)这种更新就会不向下兼容?,"""如题,我感觉这样不好。\r\n\r\n功能版本号不向下兼容能理解\r\n\r\n修复问题的版本号也不向下兼容,难以理解。""",,,,7931755,zplzpl,0,,7440,https://github.com/panjf2000/ants/issues/32,2019-04-21T07:16:26.000+00:00,2019-04-16T03:16:02.000+00:00,2019-04-21T07:16:26.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,38,
-1,434069015,134018330,33,closed,support semantic versioning.,"""建议将发布的tag兼容为semantic versioning,vX.Y.Z。go modules对此支持比较良好。\r\nhttps://semver.org/\r\nhttps://research.swtch.com/vgo-import""",,,,1284892,jjeffcaii,0,,6090,https://github.com/panjf2000/ants/issues/33,2019-04-21T08:25:20.000+00:00,2019-04-17T02:55:11.000+00:00,2019-04-21T08:25:20.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,39,
-1,435486645,134018330,34,closed,Important announcement about <ants> from author !!!,"""**Dear users of `ants`:**\r\nI am apologetically telling you that I have to dump all tags which already presents in `ants` repository.\r\n\r\nThe reason why I'm doing so is to standardize the version management with `Semantic Versioning`, which will make a formal and clear dependency management in go, for go modules, godep, or glide, etc. So I decide to start over the tag sequence, you could find more [...]
-1,461280653,134018330,35,closed,worker exit on panic,"""个人认为PanicHandler设计不妥。\r\n1.无PanicHandler时,抛出给外面的不是panic,外层感受不到。\r\n2.无论有没有PanicHandler,都会导致worker退出,最终pool阻塞住全部任务。""",,,,38849208,king526,0,,74481,https://github.com/panjf2000/ants/issues/35,2019-08-17T20:33:10.000+00:00,2019-06-27T03:11:49.000+00:00,2019-08-17T20:33:10.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,41,
-1,462631417,134018330,37,closed,请不要再随意变更版本号了。。。,"""之前用的是 3.9.9,结果今天构建出了问题,一看发现这个版本没了,变成 1.0.0。这种变更完全不考虑现有用户的情况。希望以后不要随意变更了""",,,,8923413,choleraehyq,0,,140,https://github.com/panjf2000/ants/issues/37,2019-07-01T12:37:55.000+00:00,2019-07-01T10:17:15.000+00:00,2019-07-02T10:17:31.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,43,
-1,472125082,134018330,38,closed,retrieveWorker与revertWorker之间会导致死锁,"""func (p *Pool) retrieveWorker() *Worker {\r\n\tvar w *Worker\r\n\r\n\t**p.lock.Lock()**\r\n\tidleWorkers := p.workers\r\n\tn := len(idleWorkers) - 1\r\n\tif n >= 0 {\r\n\t\tw = idleWorkers[n]\r\n\t\tidleWorkers[n] = nil\r\n\t\tp.workers = idleWorkers[:n]\r\n\t\tp.lock.Unlock()\r\n\t} else if p.Running() < p.Cap() {\r\n\t\tp.lock.Unlock()\r\n\t\tif cacheWorker := p.workerCache.Get(); cacheWorker != nil {\r\n\t\t\tw = ca [...]
-1,483164833,134018330,42,closed,带选项的初始化函数,我觉得用 functional options 更好一点,"""以下是示意代码\r\n如果用 functional options,原来的写法是\r\n```\r\nants.NewPool(10)\r\n```\r\n新的写法,如果不加 option,写法是不变的,因为 options 是作为可变参数传进去的。如果要加 option,只需要改成\r\n```\r\nants.NewPool(10, ants.WithNonblocking(true))\r\n```\r\n这样。\r\n\r\n现在是直接传一个 Option 结构体进去,所有的地方都要改,感觉很不优雅。\r\n具体 functional options 的设计可以看 rob pike 的一篇博客 https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html""",,,,8923413,choleraehyq,0 [...]
-1,483736247,134018330,43,closed,1.3.0 是不兼容更新,"""Pool 里那些暴露出来的字段(PanicHandler 之类的)都没了,这是一个不兼容更新,根据语义化版本的要求要发大版本。""",,,,8923413,choleraehyq,0,,652,https://github.com/panjf2000/ants/issues/43,2019-08-22T13:22:10.000+00:00,2019-08-22T02:29:34.000+00:00,2019-08-22T13:22:10.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,49,
-1,484311063,134018330,44,closed,1.1.1 -> 1.2.0 也是不兼容更新,"""Pool.Release 的返回值没了""",,,,8923413,choleraehyq,0,,3068,https://github.com/panjf2000/ants/issues/44,2019-08-25T06:36:14.000+00:00,2019-08-23T03:27:38.000+00:00,2019-08-25T06:36:14.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,50,
+connection_id,github_id,repo_id,milestone_id,number,state,title,body,priority,type,status,author_id,author_name,assignee_id,assignee_name,lead_time_minutes,url,closed_at,github_created_at,github_updated_at,severity,component,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark
+1,346842831,134018330,7856149,5,closed,关于 <-p.freeSignal 的疑惑,"""Hi,\r\n 我阅读了源码,对 `<-p.freeSignal` 这句代码有疑惑。 这句代码出现在了多个地方,freeSignal 的作用英文注释我是理解的,并且知道在 `putWorker` 中才进行 `p.freeSignal <- sig{}`\r\n\r\n对于下面的代码\r\n```\r\nfunc (p *Pool) getWorker() *Worker {\r\n\tvar w *Worker\r\n\twaiting := false\r\n\r\n\tp.lock.Lock()\r\n\tidleWorkers := p.workers\r\n\tn := len(idleWorkers) - 1\r\n\tif n < 0 { // 说明 pool中没有worker了\r\n\t\twaiting = p.Running() >= p.Cap()\r\n\t} else { // 说明pool中有worker\r\ [...]
+1,347255859,134018330,7856149,6,closed,死锁bug,"""func (p *Pool) getWorker() *Worker 这个函数的 199行 \r\n必须先解锁在加锁, 要不然会产生死锁\r\n\r\n\tp.lock.Unlock()\r\n\t\t<-p.freeSignal\r\n\t\tp.lock.Lock()""",,BUG,,13118848,lovelly,0,,1786,https://github.com/panjf2000/ants/issues/6,2018-08-04T10:18:41.000+00:00,2018-08-03T04:32:28.000+00:00,2018-08-04T10:18:41.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,12,
+1,348630179,134018330,7856149,7,closed,清理过期协程报错,"""你好,非常感谢提供这么好用的工具包。我在使用ants时,发现报异常。结果见下图\r\n![image](https://user-images.githubusercontent.com/4555057/43823431-98384444-9b21-11e8-880c-7458b931734a.png)\r\n日志是我在periodicallyPurge里加的调试信息\r\n![image](https://user-images.githubusercontent.com/4555057/43823534-e3c624a8-9b21-11e8-96c6-512e3e08db22.png)\r\n\r\n### 原因分析\r\n\r\n我认为可能原因是没有处理n==0的情况\r\n```\r\nif n > 0 {\r\n\tn++\r\n\tp.workers = idleWorkers[n:]\r\n}\r\n```\r\n\r\n\r\n### 测试代码\r\n` [...]
+1,356703393,134018330,7856149,10,closed,高并发下设定较小的worker数量问题,"""会存在cpu飚升的问题吧?""",,,,11763614,Moonlight-Zhao,0,,36198,https://github.com/panjf2000/ants/issues/10,2018-09-29T11:45:00.000+00:00,2018-09-04T08:26:55.000+00:00,2018-09-29T11:45:00.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,16,
+1,364361014,134018330,7856149,12,closed,潘少,更新下tag吧,"""鄙人现在在弄dep依赖管理,有用到你写的ants项目,可是你好像忘记打最新的tag了。最新的tag 3.6是指向ed55924这个提交,git上的最新代码是af376f1b这次提交,两次提交都隔了快5个月了,看到的话,麻烦打一个最新的tag吧。(手动可怜)""",,,,29452204,edcismybrother,0,,1293,https://github.com/panjf2000/ants/issues/12,2018-09-28T06:05:58.000+00:00,2018-09-27T08:32:25.000+00:00,2019-04-21T08:19:58.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,18,
+1,381941219,134018330,7856149,17,closed,关于优雅退出的问题,"""关于这个package优雅退出的问题,我看了一下Release的代码:\r\n\r\n`\r\n\t// Release Closed this pool.\r\n\tfunc (p *PoolWithFunc) Release() error {\r\n\t\tp.once.Do(func() {\r\n\t\t\tp.release <- sig{}\r\n\t\t\tp.lock.Lock()\r\n\t\t\tidleWorkers := p.workers\r\n\t\t\tfor i, w := range idleWorkers {\r\n\t\t\t\tw.args <- nil\r\n\t\t\t\tidleWorkers[i] = nil\r\n\t\t\t}\r\n\t\t\tp.workers = nil\r\n\t\t\tp.lock.Unlock()\r\n\t\t})\r\n\t\treturn nil\r\n\t}\r\n`\r\n\ [...]
+1,382039050,134018330,7856149,18,closed,go协程的理解,"""你好楼主,向您请教一个协程和线程的问题,协程基于go进程调度,线程基于系统内核调度,调度协程的过程是先调度线程后获得资源再去调度协程。\""官方解释: GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously。限制cpu数,本质上是什么,限制并行数?,并行数即同时执行数量?,执行单元即线程?,即限制最大并行线程数量?\""""",,,,13944100,LinuxForYQH,0,,20213,https://github.com/panjf2000/ants/issues/18,2018-12-03T03:53:50.000+00:00,2018-11-19T02:59:53.000+00:00,2018-12-03T03:53:50.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":" [...]
+1,382574800,134018330,7856149,20,closed,是否考虑任务支持回调函数处理失败的逻辑和任务依赖,"""#""",,,,5668717,kklinan,0,,95398,https://github.com/panjf2000/ants/issues/20,2019-01-25T15:34:03.000+00:00,2018-11-20T09:36:02.000+00:00,2019-01-25T15:34:03.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,26,
+1,388907811,134018330,7856149,21,closed,Benchmark 下直接使用 Semaphore 似乎更快呢?,"""简单跑了一下 benchmark,Semaphore 更快且很简单\r\n\r\n```bash\r\n$ go test -bench .\r\ngoos: darwin\r\ngoarch: amd64\r\npkg: github.com/panjf2000/ants\r\nBenchmarkGoroutineWithFunc-4 \t 1\t3445631705 ns/op\r\nBenchmarkSemaphoreWithFunc-4 \t 1\t1037219073 ns/op\r\nBenchmarkAntsPoolWithFunc-4 \t 1\t1138053222 ns/op\r\nBenchmarkGoroutine-4 \t 2\t 731850771 ns/op\r\nBenchmarkSemaphore-4 [...]
+1,401277739,134018330,7856149,22,closed,是否考虑 worker 中添加 PanicHandler ?,"""比方说在创建 Pool 的时候传入一个 PanicHandler,然后在每个 worker 创建的时候 recover 之后传给 PanicHandler 处理。否则池子里如果发生 panic 会直接挂掉整个进程。""",,,,8923413,choleraehyq,0,,1174,https://github.com/panjf2000/ants/issues/22,2019-01-22T05:41:34.000+00:00,2019-01-21T10:06:56.000+00:00,2019-01-22T05:41:34.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,28,
+1,402513849,134018330,7856149,24,closed,提交任务不阻塞,"""`Pool.Submit`和`PoolWithFunc.Server`提交任务,如果没有空的worker,会一直阻塞。建议增加不阻塞的接口,当前失败时直接返回错误。""",,,,5044825,tenfyzhong,0,,300032,https://github.com/panjf2000/ants/issues/24,2019-08-20T10:56:30.000+00:00,2019-01-24T02:24:13.000+00:00,2019-08-20T10:56:30.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,30,
+1,405951301,134018330,7856149,25,closed,use example errors,"""./antstest.go:37:14: cannot use syncCalculateSum (type func()) as type ants.f in argument to ants.Submit\r\n./antstest.go:45:35: cannot use func literal (type func(interface {})) as type ants.pf in argument to ants.NewPoolWithFunc\r\n""",,,,5244267,jiashiwen,0,,3088,https://github.com/panjf2000/ants/issues/25,2019-02-04T09:11:52.000+00:00,2019-02-02T05:43:38.000+00:00,2019-02-04T09:11:52.000+00:00,,,"{""ConnectionId"":1,""Owne [...]
+1,413968505,134018330,7856149,26,closed,running可能大于cap的问题,"""running与cap的比较判断与incRuning分开执行的, 可能会出现running大于cap的问题?\r\n`func (p *Pool) retrieveWorker() *Worker {\r\n\tvar w *Worker\r\n\r\n\tp.lock.Lock()\r\n\tidleWorkers := p.workers\r\n\tn := len(idleWorkers) - 1\r\n\tif n >= 0 {\r\n\t\tw = idleWorkers[n]\r\n\t\tidleWorkers[n] = nil\r\n\t\tp.workers = idleWorkers[:n]\r\n\t\tp.lock.Unlock()\r\n\t} else if p.Running() < p.Cap() {\r\n\t\tp.lock.Unlock()\r\n\t\tif cacheWorker := p.workerCac [...]
+1,419183961,134018330,7856149,27,closed,为何goroutine一直上不去,用户量也打不上去,"""为何goroutine一直上不去,用户量也打不上去\r\n是我用的有问题吗?\r\n\r\nwebsocket server\r\nhttps://github.com/im-ai/pushm/blob/master/learn/goroutine/goroutinepoolwebsocket.go\r\n\r\nwebsocket cient\r\nhttps://github.com/im-ai/pushm/blob/master/learn/goroutine/goroutinepoolwebsocketclient.go\r\n""",,,,38367404,liliang8858,0,,37496,https://github.com/panjf2000/ants/issues/27,2019-04-05T14:05:20.000+00:00,2019-03-10T13:08:52.000+00:00,2019-04-05T [...]
+1,419268851,134018330,7856149,28,closed,cap 和 running 比较的问题,"""这是我在 Playground 上面的代码 https://play.golang.org/p/D94YUU3FnX6\r\natomic 只能保证自增自减时的原子操作,在比较过程中,其他线程对变量进行了操作 比较过程并无感知,所以这个比较结果 不是完全正确的,想要实现 比较的数量完全正确,只能在修改和比较两个值的地方加锁\r\n像 #26 说的是对的""",,,,29243953,naiba,0,,237002,https://github.com/panjf2000/ants/issues/28,2019-08-22T16:27:37.000+00:00,2019-03-11T02:24:41.000+00:00,2019-08-22T16:27:37.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,34,
+1,424634533,134018330,7856149,29,closed,任务传参,"""你好,你的项目太酷了👍\r\n\r\nhttps://github.com/panjf2000/ants/blob/master/pool.go#L124 貌似不支持带参数的任务, 请问传参是用闭包的方式吗?\r\n""",,,,8509898,prprprus,0,,999,https://github.com/panjf2000/ants/issues/29,2019-03-25T09:32:11.000+00:00,2019-03-24T16:52:21.000+00:00,2019-03-25T09:45:05.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,35,
+1,429972115,134018330,7856149,31,closed,Add go.mod,"""""",,,,48135919,tsatke,0,,3474,https://github.com/panjf2000/ants/issues/31,2019-04-08T09:45:31.000+00:00,2019-04-05T23:50:36.000+00:00,2019-10-17T03:12:19.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,37,
+1,433564955,134018330,7856149,32,closed,关于版本问题,我发现小版本(0.0.x)这种更新就会不向下兼容?,"""如题,我感觉这样不好。\r\n\r\n功能版本号不向下兼容能理解\r\n\r\n修复问题的版本号也不向下兼容,难以理解。""",,,,7931755,zplzpl,0,,7440,https://github.com/panjf2000/ants/issues/32,2019-04-21T07:16:26.000+00:00,2019-04-16T03:16:02.000+00:00,2019-04-21T07:16:26.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,38,
+1,434069015,134018330,7856149,33,closed,support semantic versioning.,"""建议将发布的tag兼容为semantic versioning,vX.Y.Z。go modules对此支持比较良好。\r\nhttps://semver.org/\r\nhttps://research.swtch.com/vgo-import""",,,,1284892,jjeffcaii,0,,6090,https://github.com/panjf2000/ants/issues/33,2019-04-21T08:25:20.000+00:00,2019-04-17T02:55:11.000+00:00,2019-04-21T08:25:20.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,39,
+1,435486645,134018330,7856149,34,closed,Important announcement about <ants> from author !!!,"""**Dear users of `ants`:**\r\nI am apologetically telling you that I have to dump all tags which already presents in `ants` repository.\r\n\r\nThe reason why I'm doing so is to standardize the version management with `Semantic Versioning`, which will make a formal and clear dependency management in go, for go modules, godep, or glide, etc. So I decide to start over the tag sequence, you could fi [...]
+1,461280653,134018330,7856149,35,closed,worker exit on panic,"""个人认为PanicHandler设计不妥。\r\n1.无PanicHandler时,抛出给外面的不是panic,外层感受不到。\r\n2.无论有没有PanicHandler,都会导致worker退出,最终pool阻塞住全部任务。""",,,,38849208,king526,0,,74481,https://github.com/panjf2000/ants/issues/35,2019-08-17T20:33:10.000+00:00,2019-06-27T03:11:49.000+00:00,2019-08-17T20:33:10.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,41,
+1,462631417,134018330,7856149,37,closed,请不要再随意变更版本号了。。。,"""之前用的是 3.9.9,结果今天构建出了问题,一看发现这个版本没了,变成 1.0.0。这种变更完全不考虑现有用户的情况。希望以后不要随意变更了""",,,,8923413,choleraehyq,0,,140,https://github.com/panjf2000/ants/issues/37,2019-07-01T12:37:55.000+00:00,2019-07-01T10:17:15.000+00:00,2019-07-02T10:17:31.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,43,
+1,472125082,134018330,7856149,38,closed,retrieveWorker与revertWorker之间会导致死锁,"""func (p *Pool) retrieveWorker() *Worker {\r\n\tvar w *Worker\r\n\r\n\t**p.lock.Lock()**\r\n\tidleWorkers := p.workers\r\n\tn := len(idleWorkers) - 1\r\n\tif n >= 0 {\r\n\t\tw = idleWorkers[n]\r\n\t\tidleWorkers[n] = nil\r\n\t\tp.workers = idleWorkers[:n]\r\n\t\tp.lock.Unlock()\r\n\t} else if p.Running() < p.Cap() {\r\n\t\tp.lock.Unlock()\r\n\t\tif cacheWorker := p.workerCache.Get(); cacheWorker != nil {\r\n\t\t [...]
+1,483164833,134018330,7856149,42,closed,带选项的初始化函数,我觉得用 functional options 更好一点,"""以下是示意代码\r\n如果用 functional options,原来的写法是\r\n```\r\nants.NewPool(10)\r\n```\r\n新的写法,如果不加 option,写法是不变的,因为 options 是作为可变参数传进去的。如果要加 option,只需要改成\r\n```\r\nants.NewPool(10, ants.WithNonblocking(true))\r\n```\r\n这样。\r\n\r\n现在是直接传一个 Option 结构体进去,所有的地方都要改,感觉很不优雅。\r\n具体 functional options 的设计可以看 rob pike 的一篇博客 https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html""",,,,8923413,chole [...]
+1,483736247,134018330,7856149,43,closed,1.3.0 是不兼容更新,"""Pool 里那些暴露出来的字段(PanicHandler 之类的)都没了,这是一个不兼容更新,根据语义化版本的要求要发大版本。""",,,,8923413,choleraehyq,0,,652,https://github.com/panjf2000/ants/issues/43,2019-08-22T13:22:10.000+00:00,2019-08-22T02:29:34.000+00:00,2019-08-22T13:22:10.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,49,
+1,484311063,134018330,7856149,44,closed,1.1.1 -> 1.2.0 也是不兼容更新,"""Pool.Release 的返回值没了""",,,,8923413,choleraehyq,0,,3068,https://github.com/panjf2000/ants/issues/44,2019-08-25T06:36:14.000+00:00,2019-08-23T03:27:38.000+00:00,2019-08-25T06:36:14.000+00:00,,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_api_issues,50,
diff --git a/plugins/github/e2e/snapshot_tables/_tool_github_milestones.csv b/plugins/github/e2e/snapshot_tables/_tool_github_milestones.csv
new file mode 100644
index 00000000..75dfe942
--- /dev/null
+++ b/plugins/github/e2e/snapshot_tables/_tool_github_milestones.csv
@@ -0,0 +1,2 @@
+connection_id,milestone_id,repo_id,number,url,title,open_issues,closed_issues,state,created_at,updated_at,closed_at,_raw_data_params,_raw_data_table,_raw_data_id,_raw_data_remark
+1,7856149,134018330,7,https://api.github.com/repos/apache/incubator-devlake/milestones/7,v0.11.0,2,118,open,2022-04-08T02:05:35.000+00:00,2022-06-24T01:34:37.000+00:00,,"{""ConnectionId"":1,""Owner"":""panjf2000"",""Repo"":""ants""}",_raw_github_milestones,109,
diff --git a/plugins/github/e2e/snapshot_tables/board_sprint.csv b/plugins/github/e2e/snapshot_tables/board_sprint.csv
new file mode 100644
index 00000000..0f45a0ae
--- /dev/null
+++ b/plugins/github/e2e/snapshot_tables/board_sprint.csv
@@ -0,0 +1,2 @@
+board_id,sprint_id
+github:GithubRepo:1:134018330,github:GithubMilestone:1:7856149
diff --git a/plugins/github/e2e/snapshot_tables/boards.csv b/plugins/github/e2e/snapshot_tables/boards.csv
index 7201be87..e98f6bfa 100644
--- a/plugins/github/e2e/snapshot_tables/boards.csv
+++ b/plugins/github/e2e/snapshot_tables/boards.csv
@@ -1,2 +1,2 @@
id,name,description,url,created_date
-github:GithubRepo:1:134018330,panjf2000/ants,"🐜🐜🐜 ants is a high-performance and low-cost goroutine pool in Go, inspired by fasthttp./ ants 是一个高性能且低损耗的 goroutine 池。",https://github.com/panjf2000/ants/issues,2018-05-19T01:13:38.000+00:00
+github:GithubRepo:1:134018330,panjf2000/ants,"🐜🐜🐜 ants is a high-performance and low-cost goroutine pool in Go, inspired by fasthttp./ ants 是一个高性能且低损耗的 goroutine 池。",https://github.com/panjf2000/ants/issues,2018-05-19T01:13:38.000+00:00
\ No newline at end of file
diff --git a/plugins/github/e2e/snapshot_tables/sprint_issue.csv b/plugins/github/e2e/snapshot_tables/sprint_issue.csv
new file mode 100644
index 00000000..153ed83d
--- /dev/null
+++ b/plugins/github/e2e/snapshot_tables/sprint_issue.csv
@@ -0,0 +1,27 @@
+sprint_id,issue_id
+github:GithubMilestone:1:7856149,github:GithubIssue:1:346842831
+github:GithubMilestone:1:7856149,github:GithubIssue:1:347255859
+github:GithubMilestone:1:7856149,github:GithubIssue:1:348630179
+github:GithubMilestone:1:7856149,github:GithubIssue:1:356703393
+github:GithubMilestone:1:7856149,github:GithubIssue:1:364361014
+github:GithubMilestone:1:7856149,github:GithubIssue:1:381941219
+github:GithubMilestone:1:7856149,github:GithubIssue:1:382039050
+github:GithubMilestone:1:7856149,github:GithubIssue:1:382574800
+github:GithubMilestone:1:7856149,github:GithubIssue:1:388907811
+github:GithubMilestone:1:7856149,github:GithubIssue:1:401277739
+github:GithubMilestone:1:7856149,github:GithubIssue:1:402513849
+github:GithubMilestone:1:7856149,github:GithubIssue:1:405951301
+github:GithubMilestone:1:7856149,github:GithubIssue:1:413968505
+github:GithubMilestone:1:7856149,github:GithubIssue:1:419183961
+github:GithubMilestone:1:7856149,github:GithubIssue:1:419268851
+github:GithubMilestone:1:7856149,github:GithubIssue:1:424634533
+github:GithubMilestone:1:7856149,github:GithubIssue:1:429972115
+github:GithubMilestone:1:7856149,github:GithubIssue:1:433564955
+github:GithubMilestone:1:7856149,github:GithubIssue:1:434069015
+github:GithubMilestone:1:7856149,github:GithubIssue:1:435486645
+github:GithubMilestone:1:7856149,github:GithubIssue:1:461280653
+github:GithubMilestone:1:7856149,github:GithubIssue:1:462631417
+github:GithubMilestone:1:7856149,github:GithubIssue:1:472125082
+github:GithubMilestone:1:7856149,github:GithubIssue:1:483164833
+github:GithubMilestone:1:7856149,github:GithubIssue:1:483736247
+github:GithubMilestone:1:7856149,github:GithubIssue:1:484311063
diff --git a/plugins/github/e2e/snapshot_tables/sprints.csv b/plugins/github/e2e/snapshot_tables/sprints.csv
new file mode 100644
index 00000000..92f3df97
--- /dev/null
+++ b/plugins/github/e2e/snapshot_tables/sprints.csv
@@ -0,0 +1,2 @@
+id,name,url,status,started_date,ended_date,completed_date,original_board_id
+github:GithubMilestone:1:7856149,v0.11.0,https://api.github.com/repos/apache/incubator-devlake/milestones/7,open,2022-04-08T02:05:35.000+00:00,,,github:GithubRepo:1:134018330
diff --git a/plugins/github/impl/impl.go b/plugins/github/impl/impl.go
index 7f5a5ccd..8ade2967 100644
--- a/plugins/github/impl/impl.go
+++ b/plugins/github/impl/impl.go
@@ -18,6 +18,7 @@ limitations under the License.
package impl
import (
+ "fmt"
"github.com/apache/incubator-devlake/migration"
"github.com/apache/incubator-devlake/plugins/core"
"github.com/apache/incubator-devlake/plugins/github/api"
@@ -67,6 +68,8 @@ func (plugin Github) SubTaskMetas() []core.SubTaskMeta {
tasks.ExtractApiCommitsMeta,
tasks.CollectApiCommitStatsMeta,
tasks.ExtractApiCommitStatsMeta,
+ tasks.CollectMilestonesMeta,
+ tasks.ExtractMilestonesMeta,
tasks.EnrichPullRequestIssuesMeta,
tasks.ConvertRepoMeta,
tasks.ConvertIssuesMeta,
@@ -79,6 +82,7 @@ func (plugin Github) SubTaskMetas() []core.SubTaskMeta {
tasks.ConvertUsersMeta,
tasks.ConvertIssueCommentsMeta,
tasks.ConvertPullRequestCommentsMeta,
+ tasks.ConvertMilestonesMeta,
}
}
@@ -94,12 +98,12 @@ func (plugin Github) PrepareTaskData(taskCtx core.TaskContext, options map[strin
connection := &models.GithubConnection{}
err = connectionHelper.FirstById(connection, op.ConnectionId)
if err != nil {
- return err, nil
+ return nil, fmt.Errorf("unable to get github connection by the given connection ID: %v", err)
}
apiClient, err := tasks.CreateApiClient(taskCtx, connection)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("unable to get github API client instance: %v", err)
}
return &tasks.GithubTaskData{
diff --git a/plugins/github/models/issue.go b/plugins/github/models/issue.go
index b508e55d..c658d416 100644
--- a/plugins/github/models/issue.go
+++ b/plugins/github/models/issue.go
@@ -37,6 +37,7 @@ type GithubIssue struct {
AuthorName string `gorm:"type:varchar(255)"`
AssigneeId int
AssigneeName string `gorm:"type:varchar(255)"`
+ MilestoneId int `gorm:"index"`
LeadTimeMinutes uint
Url string `gorm:"type:varchar(255)"`
ClosedAt *time.Time
diff --git a/plugins/github/models/migrationscripts/register.go b/plugins/github/models/migrationscripts/register.go
index 51152508..e0b31a88 100644
--- a/plugins/github/models/migrationscripts/register.go
+++ b/plugins/github/models/migrationscripts/register.go
@@ -17,11 +17,14 @@ limitations under the License.
package migrationscripts
-import "github.com/apache/incubator-devlake/migration"
+import (
+ "github.com/apache/incubator-devlake/migration"
+)
// All return all the migration scripts
func All() []migration.Script {
return []migration.Script{
new(initSchemas),
+ new(UpdateSchemas20220620),
}
}
diff --git a/plugins/github/models/migrationscripts/updateSchemas20220620.go b/plugins/github/models/migrationscripts/updateSchemas20220620.go
new file mode 100644
index 00000000..c5178cfa
--- /dev/null
+++ b/plugins/github/models/migrationscripts/updateSchemas20220620.go
@@ -0,0 +1,73 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+ "context"
+ "github.com/apache/incubator-devlake/models/migrationscripts/archived"
+ "gorm.io/gorm"
+ "time"
+)
+
+// GithubMilestone20220620 new struct for milestones
+type GithubMilestone20220620 struct {
+ archived.NoPKModel
+ ConnectionId uint64 `gorm:"primaryKey"`
+ MilestoneId int `gorm:"primaryKey;autoIncrement:false"`
+ RepoId int
+ Number int
+ URL string
+ OpenIssues int
+ ClosedIssues int
+ State string
+ Title string
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ ClosedAt time.Time
+}
+
+// GithubIssue20220620 new field for models.GithubIssue
+type GithubIssue20220620 struct {
+ MilestoneId int
+}
+
+type UpdateSchemas20220620 struct{}
+
+func (GithubMilestone20220620) TableName() string {
+ return "_tool_github_milestones"
+}
+
+func (GithubIssue20220620) TableName() string {
+ return "_tool_github_issues"
+}
+
+func (*UpdateSchemas20220620) Up(_ context.Context, db *gorm.DB) error {
+ err := db.Migrator().AddColumn(GithubIssue20220620{}, "milestone_id")
+ if err != nil {
+ return err
+ }
+ return db.Migrator().CreateTable(GithubMilestone20220620{})
+}
+
+func (*UpdateSchemas20220620) Version() uint64 {
+ return 20220620000001
+}
+
+func (*UpdateSchemas20220620) Name() string {
+ return "Add milestone for github"
+}
diff --git a/plugins/github/models/migrationscripts/register.go b/plugins/github/models/milestone.go
similarity index 59%
copy from plugins/github/models/migrationscripts/register.go
copy to plugins/github/models/milestone.go
index 51152508..561cbfa5 100644
--- a/plugins/github/models/migrationscripts/register.go
+++ b/plugins/github/models/milestone.go
@@ -15,13 +15,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-package migrationscripts
+package models
-import "github.com/apache/incubator-devlake/migration"
+import (
+ "github.com/apache/incubator-devlake/models/common"
+ "time"
+)
-// All return all the migration scripts
-func All() []migration.Script {
- return []migration.Script{
- new(initSchemas),
- }
+type GithubMilestone struct {
+ ConnectionId uint64 `gorm:"primaryKey"`
+ MilestoneId int `gorm:"primaryKey;autoIncrement:false"`
+ RepoId int
+ Number int
+ URL string
+ Title string
+ OpenIssues int
+ ClosedIssues int
+ State string
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ ClosedAt *time.Time
+ common.NoPKModel
+}
+
+func (GithubMilestone) TableName() string {
+ return "_tool_github_milestones"
}
diff --git a/plugins/github/tasks/issue_collector.go b/plugins/github/tasks/issue_collector.go
index 35f618d1..ae735f12 100644
--- a/plugins/github/tasks/issue_collector.go
+++ b/plugins/github/tasks/issue_collector.go
@@ -114,7 +114,7 @@ func CollectApiIssues(taskCtx core.SubTaskContext) error {
query.Set("direction", "asc")
query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
query.Set("per_page", fmt.Sprintf("%v", reqData.Pager.Size))
-
+ query.Set("milestone", "*")
return query, nil
},
/*
diff --git a/plugins/github/tasks/issue_extractor.go b/plugins/github/tasks/issue_extractor.go
index efbbf54a..f5799c8b 100644
--- a/plugins/github/tasks/issue_extractor.go
+++ b/plugins/github/tasks/issue_extractor.go
@@ -51,67 +51,33 @@ type IssuesResponse struct {
Labels []struct {
Name string `json:"name"`
} `json:"labels"`
-
- Assignee *GithubUserResponse
- User *GithubUserResponse
+ Assignee *GithubUserResponse
+ User *GithubUserResponse
+ Milestone *struct {
+ Id int
+ }
ClosedAt *helper.Iso8601Time `json:"closed_at"`
GithubCreatedAt helper.Iso8601Time `json:"created_at"`
GithubUpdatedAt helper.Iso8601Time `json:"updated_at"`
}
+type IssueRegexes struct {
+ SeverityRegex *regexp.Regexp
+ ComponentRegex *regexp.Regexp
+ PriorityRegex *regexp.Regexp
+ TypeBugRegex *regexp.Regexp
+ TypeRequirementRegex *regexp.Regexp
+ TypeIncidentRegex *regexp.Regexp
+}
+
func ExtractApiIssues(taskCtx core.SubTaskContext) error {
data := taskCtx.GetData().(*GithubTaskData)
+
config := data.Options.TransformationRules
- var issueSeverityRegex *regexp.Regexp
- var issueComponentRegex *regexp.Regexp
- var issuePriorityRegex *regexp.Regexp
- var issueTypeBugRegex *regexp.Regexp
- var issueTypeRequirementRegex *regexp.Regexp
- var issueTypeIncidentRegex *regexp.Regexp
- var issueSeverity = config.IssueSeverity
- var err error
- if len(issueSeverity) > 0 {
- issueSeverityRegex, err = regexp.Compile(issueSeverity)
- if err != nil {
- return fmt.Errorf("regexp Compile issueSeverity failed:[%s] stack:[%s]", err.Error(), debug.Stack())
- }
- }
- var issueComponent = config.IssueComponent
- if len(issueComponent) > 0 {
- issueComponentRegex, err = regexp.Compile(issueComponent)
- if err != nil {
- return fmt.Errorf("regexp Compile issueComponent failed:[%s] stack:[%s]", err.Error(), debug.Stack())
- }
- }
- var issuePriority = config.IssuePriority
- if len(issuePriority) > 0 {
- issuePriorityRegex, err = regexp.Compile(issuePriority)
- if err != nil {
- return fmt.Errorf("regexp Compile issuePriority failed:[%s] stack:[%s]", err.Error(), debug.Stack())
- }
- }
- var issueTypeBug = config.IssueTypeBug
- if len(issueTypeBug) > 0 {
- issueTypeBugRegex, err = regexp.Compile(issueTypeBug)
- if err != nil {
- return fmt.Errorf("regexp Compile issueTypeBug failed:[%s] stack:[%s]", err.Error(), debug.Stack())
- }
- }
- var issueTypeRequirement = config.IssueTypeRequirement
- if len(issueTypeRequirement) > 0 {
- issueTypeRequirementRegex, err = regexp.Compile(issueTypeRequirement)
- if err != nil {
- return fmt.Errorf("regexp Compile issueTypeRequirement failed:[%s] stack:[%s]", err.Error(), debug.Stack())
- }
- }
- var issueTypeIncident = config.IssueTypeIncident
- if len(issueTypeIncident) > 0 {
- issueTypeIncidentRegex, err = regexp.Compile(issueTypeIncident)
- if err != nil {
- return fmt.Errorf("regexp Compile issueTypeIncident failed:[%s] stack:[%s]", err.Error(), debug.Stack())
- }
+ issueRegexes, err := NewIssueRegexes(config)
+ if err != nil {
+ return nil
}
-
extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
Ctx: taskCtx,
@@ -144,13 +110,18 @@ func ExtractApiIssues(taskCtx core.SubTaskContext) error {
return nil, nil
}
results := make([]interface{}, 0, 2)
+
githubIssue, err := convertGithubIssue(body, data.Options.ConnectionId, data.Repo.GithubId)
if err != nil {
return nil, err
}
+ githubLabels, err := convertGithubLabels(issueRegexes, body, githubIssue)
+ if err != nil {
+ return nil, err
+ }
+ results = append(results, githubLabels...)
+ results = append(results, githubIssue)
if body.Assignee != nil {
- githubIssue.AssigneeId = body.Assignee.Id
- githubIssue.AssigneeName = body.Assignee.Login
relatedUser, err := convertUser(body.Assignee, data.Options.ConnectionId)
if err != nil {
return nil, err
@@ -158,71 +129,22 @@ func ExtractApiIssues(taskCtx core.SubTaskContext) error {
results = append(results, relatedUser)
}
if body.User != nil {
- githubIssue.AuthorId = body.User.Id
- githubIssue.AuthorName = body.User.Login
relatedUser, err := convertUser(body.User, data.Options.ConnectionId)
if err != nil {
return nil, err
}
results = append(results, relatedUser)
}
- for _, label := range body.Labels {
- results = append(results, &models.GithubIssueLabel{
- ConnectionId: data.Options.ConnectionId,
- IssueId: githubIssue.GithubId,
- LabelName: label.Name,
- })
- if issueSeverityRegex != nil {
- groups := issueSeverityRegex.FindStringSubmatch(label.Name)
- if len(groups) > 0 {
- githubIssue.Severity = groups[1]
- }
- }
-
- if issueComponentRegex != nil {
- groups := issueComponentRegex.FindStringSubmatch(label.Name)
- if len(groups) > 0 {
- githubIssue.Component = groups[1]
- }
- }
-
- if issuePriorityRegex != nil {
- groups := issuePriorityRegex.FindStringSubmatch(label.Name)
- if len(groups) > 0 {
- githubIssue.Priority = groups[1]
- }
- }
-
- if issueTypeBugRegex != nil {
- if ok := issueTypeBugRegex.MatchString(label.Name); ok {
- githubIssue.Type = ticket.BUG
- }
- }
-
- if issueTypeRequirementRegex != nil {
- if ok := issueTypeRequirementRegex.MatchString(label.Name); ok {
- githubIssue.Type = ticket.REQUIREMENT
- }
- }
-
- if issueTypeIncidentRegex != nil {
- if ok := issueTypeIncidentRegex.MatchString(label.Name); ok {
- githubIssue.Type = ticket.INCIDENT
- }
- }
- }
- results = append(results, githubIssue)
-
return results, nil
},
})
-
if err != nil {
return err
}
return extractor.Execute()
}
+
func convertGithubIssue(issue *IssuesResponse, connectionId uint64, repositoryId int) (*models.GithubIssue, error) {
githubIssue := &models.GithubIssue{
ConnectionId: connectionId,
@@ -233,13 +155,122 @@ func convertGithubIssue(issue *IssuesResponse, connectionId uint64, repositoryId
Title: issue.Title,
Body: string(issue.Body),
Url: issue.HtmlUrl,
+ MilestoneId: issue.Milestone.Id,
ClosedAt: helper.Iso8601TimeToTime(issue.ClosedAt),
GithubCreatedAt: issue.GithubCreatedAt.ToTime(),
GithubUpdatedAt: issue.GithubUpdatedAt.ToTime(),
}
+ if issue.Assignee != nil {
+ githubIssue.AssigneeId = issue.Assignee.Id
+ githubIssue.AssigneeName = issue.Assignee.Login
+ }
+ if issue.User != nil {
+ githubIssue.AuthorId = issue.User.Id
+ githubIssue.AuthorName = issue.User.Login
+ }
if issue.ClosedAt != nil {
githubIssue.LeadTimeMinutes = uint(issue.ClosedAt.ToTime().Sub(issue.GithubCreatedAt.ToTime()).Minutes())
}
-
+ if issue.Assignee != nil {
+ githubIssue.AssigneeId = issue.Assignee.Id
+ githubIssue.AssigneeName = issue.Assignee.Login
+ }
+ if issue.User != nil {
+ githubIssue.AuthorId = issue.User.Id
+ githubIssue.AuthorName = issue.User.Login
+ }
return githubIssue, nil
}
+
+func convertGithubLabels(issueRegexes *IssueRegexes, issue *IssuesResponse, githubIssue *models.GithubIssue) ([]interface{}, error) {
+ var results []interface{}
+ for _, label := range issue.Labels {
+ results = append(results, &models.GithubIssueLabel{
+ ConnectionId: githubIssue.ConnectionId,
+ IssueId: githubIssue.GithubId,
+ LabelName: label.Name,
+ })
+ if issueRegexes.SeverityRegex != nil {
+ groups := issueRegexes.SeverityRegex.FindStringSubmatch(label.Name)
+ if len(groups) > 0 {
+ githubIssue.Severity = groups[1]
+ }
+ }
+ if issueRegexes.ComponentRegex != nil {
+ groups := issueRegexes.ComponentRegex.FindStringSubmatch(label.Name)
+ if len(groups) > 0 {
+ githubIssue.Component = groups[1]
+ }
+ }
+ if issueRegexes.PriorityRegex != nil {
+ groups := issueRegexes.PriorityRegex.FindStringSubmatch(label.Name)
+ if len(groups) > 0 {
+ githubIssue.Priority = groups[1]
+ }
+ }
+ if issueRegexes.TypeBugRegex != nil {
+ if ok := issueRegexes.TypeBugRegex.MatchString(label.Name); ok {
+ githubIssue.Type = ticket.BUG
+ }
+ }
+ if issueRegexes.TypeRequirementRegex != nil {
+ if ok := issueRegexes.TypeRequirementRegex.MatchString(label.Name); ok {
+ githubIssue.Type = ticket.REQUIREMENT
+ }
+ }
+ if issueRegexes.TypeIncidentRegex != nil {
+ if ok := issueRegexes.TypeIncidentRegex.MatchString(label.Name); ok {
+ githubIssue.Type = ticket.INCIDENT
+ }
+ }
+ }
+ return results, nil
+}
+
+func NewIssueRegexes(config models.TransformationRules) (*IssueRegexes, error) {
+ var issueRegexes IssueRegexes
+ var issueSeverity = config.IssueSeverity
+ var err error
+ if len(issueSeverity) > 0 {
+ issueRegexes.SeverityRegex, err = regexp.Compile(issueSeverity)
+ if err != nil {
+ return nil, fmt.Errorf("regexp Compile issueSeverity failed:[%s] stack:[%s]", err.Error(), debug.Stack())
+ }
+ }
+ var issueComponent = config.IssueComponent
+ if len(issueComponent) > 0 {
+ issueRegexes.ComponentRegex, err = regexp.Compile(issueComponent)
+ if err != nil {
+ return nil, fmt.Errorf("regexp Compile issueComponent failed:[%s] stack:[%s]", err.Error(), debug.Stack())
+ }
+ }
+ var issuePriority = config.IssuePriority
+ if len(issuePriority) > 0 {
+ issueRegexes.PriorityRegex, err = regexp.Compile(issuePriority)
+ if err != nil {
+ return nil, fmt.Errorf("regexp Compile issuePriority failed:[%s] stack:[%s]", err.Error(), debug.Stack())
+ }
+ }
+ var issueTypeBug = config.IssueTypeBug
+ if len(issueTypeBug) > 0 {
+ issueRegexes.TypeBugRegex, err = regexp.Compile(issueTypeBug)
+ if err != nil {
+ return nil, fmt.Errorf("regexp Compile issueTypeBug failed:[%s] stack:[%s]", err.Error(), debug.Stack())
+ }
+ }
+ var issueTypeRequirement = config.IssueTypeRequirement
+ if len(issueTypeRequirement) > 0 {
+ issueRegexes.TypeRequirementRegex, err = regexp.Compile(issueTypeRequirement)
+ if err != nil {
+ return nil, fmt.Errorf("regexp Compile issueTypeRequirement failed:[%s] stack:[%s]", err.Error(), debug.Stack())
+ }
+ }
+ var issueTypeIncident = config.IssueTypeIncident
+ if len(issueTypeIncident) > 0 {
+ issueRegexes.TypeIncidentRegex, err = regexp.Compile(issueTypeIncident)
+ if err != nil {
+ return nil, fmt.Errorf("regexp Compile issueTypeIncident failed:[%s] stack:[%s]", err.Error(), debug.Stack())
+ }
+ }
+ return &issueRegexes, nil
+}
diff --git a/plugins/github/tasks/milestone_collector.go b/plugins/github/tasks/milestone_collector.go
new file mode 100644
index 00000000..ff43bd10
--- /dev/null
+++ b/plugins/github/tasks/milestone_collector.go
@@ -0,0 +1,78 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/apache/incubator-devlake/plugins/helper"
+
+ "github.com/apache/incubator-devlake/plugins/core"
+)
+
+const RAW_MILESTONE_TABLE = "github_milestones"
+
+var CollectMilestonesMeta = core.SubTaskMeta{
+ Name: "collectApiMilestones",
+ EntryPoint: CollectApiMilestones,
+ EnabledByDefault: true,
+ Description: "Collect milestone data from Github api",
+}
+
+func CollectApiMilestones(taskCtx core.SubTaskContext) error {
+ data := taskCtx.GetData().(*GithubTaskData)
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: GithubApiParams{
+ Owner: data.Options.Owner,
+ Repo: data.Options.Repo,
+ },
+ Table: RAW_MILESTONE_TABLE,
+ },
+ ApiClient: data.ApiClient,
+ PageSize: 100,
+ Incremental: false,
+ UrlTemplate: "repos/{{ .Params.Owner }}/{{ .Params.Repo }}/milestones",
+ Query: func(reqData *helper.RequestData) (url.Values, error) {
+ query := url.Values{}
+ query.Set("state", "all")
+ query.Set("direction", "asc")
+ query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+ query.Set("per_page", fmt.Sprintf("%v", reqData.Pager.Size))
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage, error) {
+ var items []json.RawMessage
+ err := helper.UnmarshalResponse(res, &items)
+ if err != nil {
+ return nil, err
+ }
+ return items, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+ return collector.Execute()
+}
diff --git a/plugins/github/tasks/milestone_converter.go b/plugins/github/tasks/milestone_converter.go
new file mode 100644
index 00000000..069f9623
--- /dev/null
+++ b/plugins/github/tasks/milestone_converter.go
@@ -0,0 +1,109 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "github.com/apache/incubator-devlake/models/common"
+ "github.com/apache/incubator-devlake/models/domainlayer"
+ "github.com/apache/incubator-devlake/models/domainlayer/didgen"
+ "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/core/dal"
+ githubModels "github.com/apache/incubator-devlake/plugins/github/models"
+ "github.com/apache/incubator-devlake/plugins/helper"
+ "reflect"
+)
+
+var ConvertMilestonesMeta = core.SubTaskMeta{
+ Name: "convertMilestones",
+ EntryPoint: ConvertMilestones,
+ EnabledByDefault: true,
+ Description: "Convert tool layer table github_milestones into domain layer table milestones",
+}
+
+type MilestoneConverterModel struct {
+ common.RawDataOrigin
+ githubModels.GithubMilestone
+ GithubId int
+}
+
+func ConvertMilestones(taskCtx core.SubTaskContext) error {
+ data := taskCtx.GetData().(*GithubTaskData)
+ repoId := data.Repo.GithubId
+ connectionId := data.Options.ConnectionId
+ db := taskCtx.GetDal()
+ clauses := []dal.Clause{
+ dal.Select("gi.github_id, gm.*"),
+ dal.From("_tool_github_issues gi"),
+ dal.Join("JOIN _tool_github_milestones gm ON gm.milestone_id = gi.milestone_id"),
+ dal.Where("gm.repo_id = ?", repoId),
+ }
+ cursor, err := db.Cursor(clauses...)
+ if err != nil {
+ return err
+ }
+ defer cursor.Close()
+
+ boardIdGen := didgen.NewDomainIdGenerator(&githubModels.GithubRepo{})
+ domainBoardId := boardIdGen.Generate(connectionId, repoId)
+ sprintIdGen := didgen.NewDomainIdGenerator(&githubModels.GithubMilestone{})
+ issueIdGen := didgen.NewDomainIdGenerator(&githubModels.GithubIssue{})
+
+ converter, err := helper.NewDataConverter(helper.DataConverterArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: GithubApiParams{
+ ConnectionId: connectionId,
+ Owner: data.Options.Owner,
+ Repo: data.Options.Repo,
+ },
+ Table: RAW_MILESTONE_TABLE,
+ },
+ InputRowType: reflect.TypeOf(MilestoneConverterModel{}),
+ Input: cursor,
+ Convert: func(inputRow interface{}) ([]interface{}, error) {
+ response := inputRow.(*MilestoneConverterModel)
+ domainSprintId := sprintIdGen.Generate(connectionId, response.GithubMilestone.MilestoneId)
+ domainIssueId := issueIdGen.Generate(connectionId, response.GithubId)
+ sprint := &ticket.Sprint{
+ DomainEntity: domainlayer.DomainEntity{Id: domainSprintId},
+ Name: response.GithubMilestone.Title,
+ Url: response.GithubMilestone.URL,
+ Status: response.GithubMilestone.State,
+ StartedDate: &response.GithubMilestone.CreatedAt, //GitHub doesn't give us a "start date"
+ EndedDate: response.GithubMilestone.ClosedAt,
+ CompletedDate: response.GithubMilestone.ClosedAt,
+ OriginalBoardID: domainBoardId,
+ }
+ boardSprint := &ticket.BoardSprint{
+ BoardId: domainBoardId,
+ SprintId: domainSprintId,
+ }
+ sprintIssue := &ticket.SprintIssue{
+ SprintId: domainSprintId,
+ IssueId: domainIssueId,
+ }
+ return []interface{}{sprint, sprintIssue, boardSprint}, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return converter.Execute()
+}
diff --git a/plugins/github/tasks/milestone_extractor.go b/plugins/github/tasks/milestone_extractor.go
new file mode 100644
index 00000000..fc61aa96
--- /dev/null
+++ b/plugins/github/tasks/milestone_extractor.go
@@ -0,0 +1,118 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "github.com/apache/incubator-devlake/plugins/core"
+ "github.com/apache/incubator-devlake/plugins/github/models"
+ "github.com/apache/incubator-devlake/plugins/helper"
+)
+
+var ExtractMilestonesMeta = core.SubTaskMeta{
+ Name: "extractMilestones",
+ EntryPoint: ExtractMilestones,
+ EnabledByDefault: true,
+ Description: "Extract raw milestone data into tool layer table github_milestones",
+}
+
+type MilestonesResponse struct {
+ Url string `json:"url"`
+ HtmlUrl string `json:"html_url"`
+ LabelsUrl string `json:"labels_url"`
+ Id int `json:"id"`
+ NodeId string `json:"node_id"`
+ Number int `json:"number"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Creator struct {
+ Login string `json:"login"`
+ Id int `json:"id"`
+ NodeId string `json:"node_id"`
+ AvatarUrl string `json:"avatar_url"`
+ GravatarId string `json:"gravatar_id"`
+ Url string `json:"url"`
+ HtmlUrl string `json:"html_url"`
+ FollowersUrl string `json:"followers_url"`
+ FollowingUrl string `json:"following_url"`
+ GistsUrl string `json:"gists_url"`
+ StarredUrl string `json:"starred_url"`
+ SubscriptionsUrl string `json:"subscriptions_url"`
+ OrganizationsUrl string `json:"organizations_url"`
+ ReposUrl string `json:"repos_url"`
+ EventsUrl string `json:"events_url"`
+ ReceivedEventsUrl string `json:"received_events_url"`
+ Type string `json:"type"`
+ SiteAdmin bool `json:"site_admin"`
+ } `json:"creator"`
+ OpenIssues int `json:"open_issues"`
+ ClosedIssues int `json:"closed_issues"`
+ State string `json:"state"`
+ CreatedAt helper.Iso8601Time `json:"created_at"`
+ UpdatedAt helper.Iso8601Time `json:"updated_at"`
+ DueOn *helper.Iso8601Time `json:"due_on"`
+ ClosedAt *helper.Iso8601Time `json:"closed_at"`
+}
+
+func ExtractMilestones(taskCtx core.SubTaskContext) error {
+ data := taskCtx.GetData().(*GithubTaskData)
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: GithubApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ Owner: data.Options.Owner,
+ Repo: data.Options.Repo,
+ },
+ Table: RAW_MILESTONE_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{}, error) {
+ response := &MilestonesResponse{}
+ err := json.Unmarshal(row.Data, response)
+ if err != nil {
+ return nil, err
+ }
+ results := make([]interface{}, 0, 1)
+ results = append(results, convertGithubMilestone(response, data.Options.ConnectionId, data.Repo.GithubId))
+ return results, nil
+ },
+ })
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
+
+func convertGithubMilestone(response *MilestonesResponse, connectionId uint64, repositoryId int) *models.GithubMilestone {
+ milestone := &models.GithubMilestone{
+ ConnectionId: connectionId,
+ MilestoneId: response.Id,
+ RepoId: repositoryId,
+ Number: response.Number,
+ URL: response.Url,
+ Title: response.Title,
+ OpenIssues: response.OpenIssues,
+ ClosedIssues: response.ClosedIssues,
+ State: response.State,
+ ClosedAt: helper.Iso8601TimeToTime(response.ClosedAt),
+ CreatedAt: response.CreatedAt.ToTime(),
+ UpdatedAt: response.UpdatedAt.ToTime(),
+ }
+ return milestone
+}
diff --git a/runner/directrun.go b/runner/directrun.go
index 4e0fa19a..05199e07 100644
--- a/runner/directrun.go
+++ b/runner/directrun.go
@@ -19,13 +19,18 @@ package runner
import (
"context"
+ "errors"
"fmt"
-
"github.com/apache/incubator-devlake/config"
"github.com/apache/incubator-devlake/logger"
"github.com/apache/incubator-devlake/migration"
"github.com/apache/incubator-devlake/plugins/core"
"github.com/spf13/cobra"
+ "io"
+ "os"
+ "os/signal"
+ "runtime"
+ "syscall"
)
func RunCmd(cmd *cobra.Command) {
@@ -72,13 +77,39 @@ func DirectRun(cmd *cobra.Command, args []string, pluginTask core.PluginTask, op
if err != nil {
panic(err)
}
+ ctx := createContext()
+ err = RunPluginSubTasks(
+ cfg,
+ log,
+ db,
+ ctx,
+ cmd.Use,
+ tasks,
+ options,
+ pluginTask,
+ nil,
+ )
+ if err != nil {
+ panic(err)
+ }
+}
+func createContext() context.Context {
ctx, cancel := context.WithCancel(context.Background())
+ sigc := make(chan os.Signal, 1)
+ signal.Notify(sigc, getStopSignals()...)
+ go func() {
+ <-sigc
+ cancel()
+ }()
go func() {
var buf string
n, err := fmt.Scan(&buf)
if err != nil {
+ if errors.Is(err, io.EOF) {
+ return
+ }
panic(err)
} else if n == 1 && buf == "c" {
cancel()
@@ -88,19 +119,16 @@ func DirectRun(cmd *cobra.Command, args []string, pluginTask core.PluginTask, op
}
}()
println("press `c` and enter to send cancel signal")
+ return ctx
+}
- err = RunPluginSubTasks(
- cfg,
- log,
- db,
- ctx,
- cmd.Use,
- tasks,
- options,
- pluginTask,
- nil,
- )
- if err != nil {
- panic(err)
+func getStopSignals() []os.Signal {
+ if runtime.GOOS == "windows" {
+ return []os.Signal{
+ syscall.Signal(0x6), //syscall.SIGABRT for windows
+ }
+ }
+ return []os.Signal{
+ syscall.Signal(0x14), //syscall.SIGTSTP for posix
}
}