├── bin └── greport ├── Screenshot.png ├── dist └── greport-macos.tar.gz ├── errors.go ├── Makefile ├── install ├── .gitignore ├── cmd └── greport │ ├── config.go │ ├── init.go │ ├── main.go │ └── generate.go ├── utils.go ├── Gopkg.lock ├── Gopkg.toml ├── report.go ├── README.md ├── config.go └── git.go /bin/greport: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanhtuan0409/git-report/HEAD/bin/greport -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanhtuan0409/git-report/HEAD/Screenshot.png -------------------------------------------------------------------------------- /dist/greport-macos.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanhtuan0409/git-report/HEAD/dist/greport-macos.tar.gz -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package gitreport 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNoFileConfig = errors.New("No file config found") 7 | ) 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | GOOS=darwin GOARCH=amd64 go build -o bin/greport github.com/vanhtuan0409/git-report/cmd/greport 3 | 4 | release: 5 | make build 6 | tar -C bin -zcvf dist/greport-macos.tar.gz greport 7 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | echo "Download binary zip" 2 | curl -L https://github.com/vanhtuan0409/git-report/releases/download/v0.1.1/greport-macos.tar.gz > greport-macos.tar.gz 3 | echo "Unzip binary file" 4 | tar -zxvf greport-macos.tar.gz 5 | rm greport-macos.tar.gz 6 | echo "Moving file to /usr/local/bin" 7 | mv greport /usr/local/bin 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | vendor 27 | 28 | \.DS_Store 29 | -------------------------------------------------------------------------------- /cmd/greport/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | gitreport "github.com/vanhtuan0409/git-report" 7 | cli "gopkg.in/urfave/cli.v1" 8 | ) 9 | 10 | func showConfig(c *cli.Context) error { 11 | configPath := gitreport.GetDefaultConfigPath() 12 | config, err := gitreport.ReadConfigFromFile(configPath) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | fmt.Println(config.ToString()) 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /cmd/greport/init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | gitreport "github.com/vanhtuan0409/git-report" 8 | cli "gopkg.in/urfave/cli.v1" 9 | ) 10 | 11 | func initConfig(c *cli.Context) error { 12 | configPath := gitreport.GetDefaultConfigPath() 13 | _, err := gitreport.CreateDefaultConfig(configPath) 14 | if err != nil { 15 | return err 16 | } 17 | fmt.Printf("Config files created at: %s\n", filepath.Dir(configPath)) 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package gitreport 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "os/user" 7 | ) 8 | 9 | func ResolvePath(path string) (string, error) { 10 | if path[0] == '.' { 11 | return "", errors.New("Must use absolute path for repo") 12 | } 13 | 14 | if path[0] == '~' { 15 | return resolveHomePath(path), nil 16 | } 17 | 18 | return path, nil 19 | } 20 | 21 | func resolveHomePath(path string) string { 22 | if path == "~" { 23 | return getUserHome() 24 | } 25 | return getUserHome() + path[1:] 26 | } 27 | 28 | func getUserHome() string { 29 | if os.Getenv("HOME") != "" { 30 | return os.Getenv("HOME") 31 | } 32 | 33 | usr, _ := user.Current() 34 | return usr.HomeDir 35 | } 36 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:b24d38b282bacf9791408a080f606370efa3d364e4b5fd9ba0f7b87786d3b679" 6 | name = "gopkg.in/urfave/cli.v1" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" 10 | version = "v1.20.0" 11 | 12 | [[projects]] 13 | digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" 14 | name = "gopkg.in/yaml.v2" 15 | packages = ["."] 16 | pruneopts = "UT" 17 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 18 | version = "v2.2.1" 19 | 20 | [solve-meta] 21 | analyzer-name = "dep" 22 | analyzer-version = 1 23 | input-imports = [ 24 | "gopkg.in/urfave/cli.v1", 25 | "gopkg.in/yaml.v2", 26 | ] 27 | solver-name = "gps-cdcl" 28 | solver-version = 1 29 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [prune] 29 | go-tests = true 30 | unused-packages = true 31 | 32 | [[constraint]] 33 | name = "gopkg.in/yaml.v2" 34 | version = "2.2.1" 35 | 36 | [[constraint]] 37 | name = "gopkg.in/urfave/cli.v1" 38 | version = "1.20.0" 39 | -------------------------------------------------------------------------------- /cmd/greport/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | cli "gopkg.in/urfave/cli.v1" 8 | ) 9 | 10 | func main() { 11 | app := cli.NewApp() 12 | app.Name = "Git Report" 13 | app.Usage = "Collect git commit messages and organize by days to create a daily report" 14 | app.Version = "v0.1.1" 15 | app.Commands = []cli.Command{ 16 | { 17 | Name: "generate", 18 | Usage: "Generate daily report from commit messages", 19 | Action: generateReport, 20 | Flags: []cli.Flag{ 21 | cli.StringFlag{ 22 | Name: "from, f", 23 | Value: "", 24 | Usage: "Get commits starting from date. Date format: YYYY-MM-DD", 25 | }, 26 | cli.StringFlag{ 27 | Name: "to, t", 28 | Value: "", 29 | Usage: "Get commits ending to date. Date format: YYYY-MM-DD", 30 | }, 31 | }, 32 | }, 33 | { 34 | Name: "init", 35 | Usage: "Initialize config file", 36 | Action: initConfig, 37 | }, 38 | { 39 | Name: "config", 40 | Usage: "Show config file", 41 | Action: showConfig, 42 | }, 43 | } 44 | 45 | if err := app.Run(os.Args); err != nil { 46 | fmt.Printf("Encountered error: %s\n", err.Error()) 47 | os.Exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /report.go: -------------------------------------------------------------------------------- 1 | package gitreport 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type IReportGenerator interface { 9 | GenerateFromResults([]*Result) string 10 | GenerateFromCommits(*Result) string 11 | } 12 | 13 | type reportGenerator struct{} 14 | 15 | func NewReportGenerator() IReportGenerator { 16 | return &reportGenerator{} 17 | } 18 | 19 | func (r *reportGenerator) GenerateFromResults(results []*Result) string { 20 | sb := new(strings.Builder) 21 | for _, result := range results { 22 | fmt.Fprint(sb, r.GenerateFromCommits(result)) 23 | } 24 | return sb.String() 25 | } 26 | 27 | func (r *reportGenerator) GenerateFromCommits(result *Result) string { 28 | sb := new(strings.Builder) 29 | fmt.Fprintf(sb, "Repository: %s\n", result.Repo) 30 | groups := groupByDay(result.Commits) 31 | for _, g := range groups { 32 | fmt.Fprintf(sb, " + %s\n", g.name) 33 | for _, commit := range g.commits { 34 | fmt.Fprintf(sb, " - %s: %s\n", commit.Author.Date.Format("15:04"), commit.Message()) 35 | } 36 | } 37 | 38 | return sb.String() 39 | } 40 | 41 | type group struct { 42 | name string 43 | commits []*GitCommit 44 | } 45 | 46 | func groupByDay(commits []*GitCommit) []*group { 47 | groups := []*group{} 48 | for _, commit := range commits { 49 | dayStr := commit.Author.Date.Format("2006-01-02") 50 | if len(groups) == 0 || groups[len(groups)-1].name != dayStr { 51 | groups = append(groups, &group{ 52 | name: dayStr, 53 | commits: []*GitCommit{commit}, 54 | }) 55 | } else { 56 | lastGroup := groups[len(groups)-1] 57 | lastGroup.commits = append(lastGroup.commits, commit) 58 | } 59 | } 60 | return groups 61 | } 62 | -------------------------------------------------------------------------------- /cmd/greport/generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | gitreport "github.com/vanhtuan0409/git-report" 8 | cli "gopkg.in/urfave/cli.v1" 9 | ) 10 | 11 | func generateReport(c *cli.Context) error { 12 | configPath := gitreport.GetDefaultConfigPath() 13 | config, err := gitreport.ReadConfigFromFile(configPath) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | fromOption := c.String("from") 19 | fromValue, err := time.Parse("2006-01-02", fromOption) 20 | if err != nil { 21 | year, month, day := time.Now().AddDate(0, 0, -config.DefaultTimeRange).Date() 22 | fromValue = time.Date(year, month, day, 0, 0, 0, 0, time.Local) 23 | } 24 | 25 | toString := c.String("to") 26 | toValue, err := time.Parse("2006-01-02", toString) 27 | if err != nil { 28 | year, month, day := time.Now().Date() 29 | toValue = time.Date(year, month, day, 23, 59, 59, 0, time.Local) 30 | } 31 | 32 | resultChan := make(chan string) 33 | errChan := make(chan error) 34 | for _, repoPath := range config.Repos { 35 | go func(path string) { 36 | gitClient := gitreport.NewGitClient(path) 37 | result, err := gitClient.Log(&gitreport.LogOption{ 38 | Authors: config.FilterEmail, 39 | FetchAllBranch: true, 40 | FilterMergeCommit: true, 41 | Since: &fromValue, 42 | Until: &toValue, 43 | }) 44 | if err != nil { 45 | errChan <- fmt.Errorf("Cannot fetch git commits from url: %s. Original Error:\n%s", path, err.Error()) 46 | return 47 | } 48 | 49 | generator := gitreport.NewReportGenerator() 50 | report := generator.GenerateFromCommits(result) 51 | resultChan <- report 52 | }(repoPath) 53 | } 54 | 55 | for i := 0; i < len(config.Repos); i++ { 56 | select { 57 | case result := <-resultChan: 58 | fmt.Println(result) 59 | case err := <-errChan: 60 | fmt.Println(err.Error()) 61 | } 62 | } 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git report (greport) 2 | 3 | Simple tools to generate daily report from multiple commits and repositories 4 | 5 | ### Installation 6 | 7 | Prerequisite 8 | 9 | - [Git](https://git-scm.com/) installed 10 | - `greport` have only been tested on MacOS 11 | 12 | Run the following script in your terminal 13 | 14 | ```sh 15 | curl -fsSL https://raw.githubusercontent.com/vanhtuan0409/git-report/master/install | sh 16 | ``` 17 | 18 | Or manually [download binary file](https://github.com/vanhtuan0409/git-report/releases) and place it into your path 19 | 20 | ### Basic usage 21 | 22 | ```sh 23 | # create default config 24 | greport init 25 | 26 | # generate report for last 7 days of current directory 27 | greport generate 28 | 29 | # generate report from a specific date 30 | greport generate -f 2018-08-01 31 | 32 | # generate report from a specific date to a specific date 33 | greport generate -f 2018-08-01 -t 2018-08-10 34 | 35 | # view current config 36 | greport config 37 | 38 | # view manual 39 | greport help 40 | greport help generate 41 | ``` 42 | 43 | ### Config 44 | 45 | By default `greport` will generate report within 7 days for all member in current directory. This behaviour can be changed by edit config file `~/.greport/config.yml` 46 | 47 | Config variable: 48 | 49 | - emails (list): list of emails. When specified `greport` will only collect commits from author with these email. 50 | - repositories (list): list of repository paths on your machine. 51 | - default_time_range (number of days): when there is no `from` option specified, `greport` will collect all commits within default_time_range to now 52 | 53 | Example config: 54 | 55 | ```yml 56 | emails: 57 | - user1@domain.com 58 | - user_alias@domain.com 59 | repositories: 60 | - path_to_repo_1 61 | - path_to_repo_2 62 | default_time_range: 7 63 | ``` 64 | 65 | ### Manual 66 | 67 | ``` 68 | NAME: 69 | Git Report - Collect git commit messages and organize by days to create a daily report 70 | 71 | USAGE: 72 | greport [global options] command [command options] [arguments...] 73 | 74 | VERSION: 75 | v0.1.0 76 | 77 | COMMANDS: 78 | generate Generate daily report from commit messages 79 | init Initialize config file 80 | config Show config file 81 | help, h Shows a list of commands or help for one command 82 | 83 | GLOBAL OPTIONS: 84 | --help, -h show help 85 | --version, -v print the version 86 | ``` 87 | 88 | ### Screenshot 89 | 90 | ![screenshot](Screenshot.png) 91 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package gitreport 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | yaml "gopkg.in/yaml.v2" 12 | ) 13 | 14 | type Config struct { 15 | FilterEmail []string `yaml:"emails"` 16 | Repos []string `yaml:"repositories"` 17 | DefaultTimeRange int `yaml:"default_time_range"` 18 | } 19 | 20 | func (c *Config) ToString() string { 21 | sb := new(strings.Builder) 22 | fmt.Fprintf(sb, "Emails:\n") 23 | for _, email := range c.FilterEmail { 24 | fmt.Fprintf(sb, " - %s\n", email) 25 | } 26 | 27 | fmt.Fprintf(sb, "Repositories:\n") 28 | for _, repo := range c.Repos { 29 | fmt.Fprintf(sb, " - %s\n", repo) 30 | } 31 | 32 | fmt.Fprintf(sb, "Default time range: %d\n", c.DefaultTimeRange) 33 | return sb.String() 34 | } 35 | 36 | func GetDefaultConfigPath() string { 37 | path, _ := ResolvePath("~/.greport/config.yml") 38 | return path 39 | } 40 | 41 | func ReadConfigFromFile(filePath string) (*Config, error) { 42 | configContent, err := ioutil.ReadFile(filePath) 43 | 44 | config := new(Config) 45 | // cannot read config file 46 | if err != nil { 47 | config, err = CreateDefaultConfig(filePath) 48 | if err != nil { 49 | return nil, err 50 | } 51 | setDefaultConfig(config) 52 | return config, nil 53 | } 54 | 55 | err = yaml.Unmarshal(configContent, config) 56 | if err != nil { 57 | return nil, err 58 | } 59 | setDefaultConfig(config) 60 | 61 | return config, nil 62 | } 63 | 64 | func CreateDefaultConfig(filePath string) (*Config, error) { 65 | directoryPath := filepath.Dir(filePath) 66 | err := os.MkdirAll(directoryPath, os.ModePerm) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 0666) 72 | if err != nil { 73 | return nil, err 74 | } 75 | defer f.Close() 76 | 77 | defaultConfig := Config{ 78 | FilterEmail: []string{}, 79 | Repos: []string{}, 80 | DefaultTimeRange: 7, 81 | } 82 | defaultEmail := getDefaultGitEmail() 83 | if defaultEmail != "" { 84 | defaultConfig.FilterEmail = []string{defaultEmail} 85 | } 86 | 87 | out, err := yaml.Marshal(defaultConfig) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | _, err = f.Write(out) 93 | return &defaultConfig, err 94 | } 95 | 96 | func setDefaultConfig(config *Config) { 97 | if config.DefaultTimeRange == 0 { 98 | config.DefaultTimeRange = 7 99 | } 100 | if len(config.Repos) == 0 { 101 | pwd, err := os.Getwd() 102 | if err == nil { 103 | config.Repos = []string{pwd} 104 | } 105 | } 106 | if len(config.FilterEmail) == 0 { 107 | defaultEmail := getDefaultGitEmail() 108 | if defaultEmail != "" { 109 | config.FilterEmail = []string{defaultEmail} 110 | } 111 | } 112 | } 113 | 114 | func getDefaultGitEmail() string { 115 | out, err := exec.Command("git", "config", "--global", "user.email").Output() 116 | if err != nil { 117 | return "" 118 | } 119 | return string(out) 120 | } 121 | -------------------------------------------------------------------------------- /git.go: -------------------------------------------------------------------------------- 1 | package gitreport 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var ( 14 | JSONOutFormat = `{%n "commit": "%H",%n "refs": "%D",%n "subject": "%s",%n "body": "%b",%n "author": {%n "name": "%aN",%n "email": "%aE",%n "date": "%aI"%n },%n "commiter": {%n "name": "%cN",%n "email": "%cE",%n "date": "%cI"%n }%n}` 15 | JSONMinOutFormat = `{"commit": "%H","refs": "%D","subject": "%s","body": "%b","author": {"name": "%aN","email": "%aE","date": "%aI"},"commiter": {"name": "%cN","email": "%cE","date": "%cI"}}` 16 | JSONOutFormatWithComma = fmt.Sprintf("%s,", JSONOutFormat) 17 | JSONMinOutFormatWithComma = fmt.Sprintf("%s,", JSONMinOutFormat) 18 | ) 19 | 20 | type LogOption struct { 21 | Authors []string 22 | Since *time.Time 23 | Until *time.Time 24 | FetchAllBranch bool 25 | Limit int 26 | FilterMergeCommit bool 27 | } 28 | 29 | type User struct { 30 | Name string `json:"name"` 31 | Email string `json:"email"` 32 | Date time.Time `json:"date"` 33 | } 34 | 35 | type GitCommit struct { 36 | Hash string `json:"commit"` 37 | Refs string `json:"refs"` 38 | Subject string `json:"subject"` 39 | Body string `json:"body"` 40 | Author *User `json:"author"` 41 | Committer *User `json:"committer"` 42 | } 43 | 44 | func (c *GitCommit) Message() string { 45 | return fmt.Sprintf("%s %s", c.Subject, c.Body) 46 | } 47 | 48 | type Result struct { 49 | Repo string 50 | Commits []*GitCommit 51 | } 52 | 53 | type GitClientOptions struct { 54 | Repo string 55 | } 56 | 57 | type IGitClient interface { 58 | Log(*LogOption) (*Result, error) 59 | } 60 | 61 | type nativeGitWrapper struct { 62 | repo string 63 | } 64 | 65 | func NewGitClient(repoPath string) IGitClient { 66 | return &nativeGitWrapper{ 67 | repo: repoPath, 68 | } 69 | } 70 | 71 | func (c *nativeGitWrapper) Log(options *LogOption) (*Result, error) { 72 | gitPath, err := ResolvePath(c.repo) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | gitOptions := []string{"log"} 78 | gitOptions = append(gitOptions, convertLogOptions(options)...) 79 | cmd := exec.Command("git", gitOptions...) 80 | cmd.Dir = gitPath 81 | 82 | out, err := cmd.Output() 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | jsonStr := string(out) 88 | jsonStr = strings.TrimSpace(jsonStr) 89 | if jsonStr == "" { 90 | return &Result{ 91 | Repo: filepath.Base(c.repo), 92 | Commits: []*GitCommit{}, 93 | }, nil 94 | } 95 | if jsonStr[len(jsonStr)-1] != ',' { 96 | return nil, errors.New("Invalid return from git log") 97 | } 98 | jsonStr = jsonStr[:len(jsonStr)-1] 99 | jsonStr = fmt.Sprintf(`[%s]`, jsonStr) 100 | 101 | var commits []*GitCommit 102 | err = json.Unmarshal([]byte(jsonStr), &commits) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return &Result{ 108 | Repo: filepath.Base(c.repo), 109 | Commits: commits, 110 | }, nil 111 | } 112 | 113 | func convertLogOptions(option *LogOption) []string { 114 | outOptions := []string{ 115 | fmt.Sprintf("--pretty=format:%s", JSONMinOutFormatWithComma), 116 | } 117 | if option.FetchAllBranch { 118 | outOptions = append(outOptions, "--all") 119 | } 120 | if option.FilterMergeCommit { 121 | outOptions = append(outOptions, "--no-merges") 122 | } 123 | if option.Limit > 0 { 124 | outOptions = append(outOptions, fmt.Sprintf("-n %d", option.Limit)) 125 | } 126 | for _, author := range option.Authors { 127 | outOptions = append(outOptions, fmt.Sprintf(`--author=%s`, author)) 128 | } 129 | if option.Since != nil { 130 | isoFormat := option.Since.Format(time.RFC3339) 131 | outOptions = append(outOptions, fmt.Sprintf(`--since="%s"`, isoFormat)) 132 | } 133 | if option.Until != nil { 134 | isoFormat := option.Until.Format(time.RFC3339) 135 | outOptions = append(outOptions, fmt.Sprintf(`--until="%s"`, isoFormat)) 136 | } 137 | return outOptions 138 | } 139 | --------------------------------------------------------------------------------