Welcome to mirror list, hosted at ThFree Co, Russian Federation.

linguist.go « linguist « internal - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 419f22993537eb1c4f58a9f03211bdd4ba432577 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package linguist

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path"

	"gitlab.com/gitlab-org/gitaly/internal/command"
	"gitlab.com/gitlab-org/gitaly/internal/config"
)

func init() {
	config.RegisterHook(LoadColors)
}

var exportedEnvVars = []string{"HOME", "PATH", "GEM_HOME", "BUNDLE_PATH", "BUNDLE_APP_CONFIG"}

var (
	colorMap = make(map[string]Language)
)

// Language is used to parse Linguist's language.json file.
type Language struct {
	Color string `json:"color"`
}

// CountPerLanguage represents a counter value (int) per language.
type CountPerLanguage map[string]int

// ByteCountPerLanguage represents a counter value (bytes) per language.
type ByteCountPerLanguage map[string]uint64

// FileListPerLanguage is used to parse Linguist's breakdown output to represent the list of files per language.
type FileListPerLanguage map[string][]string

// Stats returns the repository's language stats as reported by 'git-linguist'.
func Stats(ctx context.Context, repoPath string, commitID string) (ByteCountPerLanguage, error) {
	reader, err := startGitLinguist(ctx, repoPath, commitID, "stats")

	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(reader)
	if err != nil {
		return nil, err
	}

	stats := make(ByteCountPerLanguage)
	return stats, json.Unmarshal(data, &stats)
}

// FileCountStats returns the file counts per language
func FileCountStats(ctx context.Context, repoPath string, commitID string) (CountPerLanguage, error) {
	reader, err := startGitLinguist(ctx, repoPath, commitID, "breakdown")
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(reader)
	if err != nil {
		return nil, err
	}

	languageFiles := make(FileListPerLanguage)
	if err := json.Unmarshal(data, &languageFiles); err != nil {
		return nil, err
	}

	stats := make(CountPerLanguage)
	for lang, files := range languageFiles {
		stats[lang] = len(files)
	}

	return stats, nil
}

// Color returns the color Linguist has assigned to language.
func Color(language string) string {
	if color := colorMap[language].Color; color != "" {
		return color
	}

	colorSha := sha256.Sum256([]byte(language))
	return fmt.Sprintf("#%x", colorSha[0:3])
}

// LoadColors loads the name->color map from the Linguist gem.
func LoadColors(cfg config.Cfg) error {
	jsonReader, err := openLanguagesJSON(cfg)
	if err != nil {
		return err
	}
	defer jsonReader.Close()

	return json.NewDecoder(jsonReader).Decode(&colorMap)
}

func startGitLinguist(ctx context.Context, repoPath string, commitID string, linguistCommand string) (io.Reader, error) {
	cmd := exec.Command("bundle", "exec", "bin/ruby-cd", repoPath, "git-linguist", "--commit="+commitID, linguistCommand)
	cmd.Dir = config.Config.Ruby.Dir

	var env []string
	reader, err := command.New(ctx, cmd, nil, nil, nil, exportEnvironment(env)...)
	if err != nil {
		return nil, err
	}

	return reader, nil
}

func openLanguagesJSON(cfg config.Cfg) (io.ReadCloser, error) {
	if jsonPath := cfg.Ruby.LinguistLanguagesPath; jsonPath != "" {
		// This is a fallback for environments where dynamic discovery of the
		// linguist path via Bundler is not working for some reason, for example
		// https://gitlab.com/gitlab-org/gitaly/issues/1119.
		return os.Open(jsonPath)
	}

	linguistPathSymlink, err := ioutil.TempFile("", "gitaly-linguist-path")
	if err != nil {
		return nil, err
	}
	defer os.Remove(linguistPathSymlink.Name())

	if err := linguistPathSymlink.Close(); err != nil {
		return nil, err
	}

	// We use a symlink because we cannot trust Bundler to not print garbage
	// on its stdout.
	rubyScript := `FileUtils.ln_sf(Bundler.rubygems.find_name('github-linguist').first.full_gem_path, ARGV.first)`
	cmd := exec.Command("bundle", "exec", "ruby", "-rfileutils", "-e", rubyScript, linguistPathSymlink.Name())
	cmd.Dir = config.Config.Ruby.Dir

	// We have learned that in practice the command we are about to run is a
	// canary for Ruby/Bundler configuration problems. Including stderr and
	// stdout in the gitaly log is useful for debugging such problems.
	cmd.Stderr = os.Stderr
	cmd.Stdout = os.Stdout

	if err := cmd.Run(); err != nil {
		if exitError, ok := err.(*exec.ExitError); ok {
			err = fmt.Errorf("%v; stderr: %q", exitError, exitError.Stderr)
		}
		return nil, err
	}

	return os.Open(path.Join(linguistPathSymlink.Name(), "lib/linguist/languages.json"))
}

func exportEnvironment(env []string) []string {
	for _, envVarName := range exportedEnvVars {
		if val, ok := os.LookupEnv(envVarName); ok {
			env = append(env, fmt.Sprintf("%s=%s", envVarName, val))
		}
	}

	return env
}