this_is_ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +95 -0
- data/exe/this_is_ruby +6 -0
- data/lib/this_is_ruby/attributes_file.rb +89 -0
- data/lib/this_is_ruby/cli.rb +110 -0
- data/lib/this_is_ruby/emission.rb +16 -0
- data/lib/this_is_ruby/language_estimate.rb +63 -0
- data/lib/this_is_ruby/plan.rb +72 -0
- data/lib/this_is_ruby/repo.rb +69 -0
- data/lib/this_is_ruby/report.rb +101 -0
- data/lib/this_is_ruby/rule.rb +42 -0
- data/lib/this_is_ruby/rules.rb +136 -0
- data/lib/this_is_ruby/version.rb +5 -0
- data/lib/this_is_ruby.rb +28 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0a2047298c7f0c4729968ed6827707200b80ad5810ccde2c5fcd6fb009d4b662
|
|
4
|
+
data.tar.gz: 541e8c560fdb8441a4aa4ec0f040827a10454feb8bd467cad44840a614a05455
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f09cc8394377d79592a9e8e845a6da04c1dd4fcfa113ebebcf0460af1d8cc3b8c240f8e944048b9a471790d926e1d49123533546aaa093b111598255173de0f4
|
|
7
|
+
data.tar.gz: f4eeae23cde25381e1fd4132f6904e7c4f5a07a1980a8e7855e115d5532ad8c84fb6b25af916264b786e2ec64f2e93f1679597edddfe70c7c7251aea287f35c2
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- Recognises generated error pages, bundler and asset-pipeline output,
|
|
8
|
+
Webpacker and Vite builds, SimpleCov reports, SQL structure dumps and
|
|
9
|
+
shadcn/ui components.
|
|
10
|
+
- Writes a managed block in `.gitattributes`, leaving the rest of the file
|
|
11
|
+
untouched, and stays quiet about anything already declared there.
|
|
12
|
+
- `plan`, `apply` and `check` commands; `--all-frontend` opt-in.
|
|
13
|
+
|
|
14
|
+
## Unreleased
|
|
15
|
+
|
|
16
|
+
- Exit 1 on an unknown command or option instead of 0, so a typo in a CI step
|
|
17
|
+
fails rather than silently skipping `check`.
|
|
18
|
+
- Leave a path alone when its Linguist attribute is already decided, including
|
|
19
|
+
when it was explicitly unset (`-linguist-generated`) or made unspecified
|
|
20
|
+
(`!linguist-generated`). Writing our line after such a declaration reversed
|
|
21
|
+
it, because the last match wins.
|
|
22
|
+
- Rewrite the managed block where it already sits, so content below it stays
|
|
23
|
+
below it.
|
|
24
|
+
- Read `git rev-parse` from stdout alone: with `GIT_TRACE` set, or on any git
|
|
25
|
+
warning, the merged stream became part of the repository root.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Evil Martians
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# this_is_ruby
|
|
2
|
+
|
|
3
|
+
GitHub decides a repository's language by counting bytes. Ruby code can often be outnumbered by sheer volume of JS/TS/HTML boilerplate. So GitHub labels the repo HTML or TypeScript.
|
|
4
|
+
|
|
5
|
+
This gem gives you controls.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
group :development do
|
|
11
|
+
gem "this_is_ruby", require: false
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```console
|
|
18
|
+
$ this_is_ruby # write the managed block into .gitattributes
|
|
19
|
+
$ this_is_ruby plan # show what that would write, change nothing
|
|
20
|
+
$ this_is_ruby check # exit 1 when .gitattributes is out of date
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`check` belongs in CI
|
|
24
|
+
|
|
25
|
+
## What it recognises
|
|
26
|
+
|
|
27
|
+
| | attribute |
|
|
28
|
+
|---|---|
|
|
29
|
+
| error pages from `rails new`, verified by the comment naming their own path | `linguist-generated` |
|
|
30
|
+
| `app/assets/builds/` from jsbundling, cssbundling, dartsass, tailwindcss-rails | `linguist-generated` |
|
|
31
|
+
| `public/assets/` precompiled asset pipeline output | `linguist-generated` |
|
|
32
|
+
| `public/packs/`, `public/packs-test/` from Webpacker and Shakapacker | `linguist-generated` |
|
|
33
|
+
| `public/vite*/` from vite_rails | `linguist-generated` |
|
|
34
|
+
| SimpleCov HTML reports, found by the report's own title | `linguist-generated` |
|
|
35
|
+
| `db/structure.sql`, matching how Rails already treats `db/schema.rb` | `linguist-generated` |
|
|
36
|
+
| shadcn/ui components, when `components.json` shows a generator put them there | `linguist-vendored` |
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
By default the gem only points at files a tool produced. On [inertia-rails/react-starter-kit][irsk], where GitHub counts TypeScript at 60.6%, that clears the HTML but leaves TypeScript the primary language.
|
|
40
|
+
|
|
41
|
+
## Make it Ruby
|
|
42
|
+
|
|
43
|
+
If you think that despite a huge amount of hand-written JS/TS/HTML, the important work in your repository is Ruby, you can say so:
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
$ this_is_ruby --all-frontend
|
|
47
|
+
```
|
|
48
|
+
On the same repository that marks `app/javascript/` as `linguist-vendored`, and
|
|
49
|
+
GitHub then counts it as **82.6% Ruby, 1.1% TypeScript**. That is not an
|
|
50
|
+
estimate: [irinanazarova/react-starter-kit-this-is-ruby][demo] is a fork of the
|
|
51
|
+
kit whose only change is the sixteen lines this command wrote. Compare its
|
|
52
|
+
language bar with [the upstream one][irsk].
|
|
53
|
+
|
|
54
|
+
The one line that matters is which attribute it uses. [Generated files are suppressed in diffs][docs]; vendored files are not. So `--all-frontend` emits `linguist-vendored`, every source file keeps showing up in full in pull requests, and the only thing that changes is the color of the bar at the top of the page:
|
|
55
|
+
|
|
56
|
+
```console
|
|
57
|
+
$ git check-attr linguist-vendored linguist-generated -- app/javascript/pages/home/index.tsx
|
|
58
|
+
app/javascript/pages/home/index.tsx: linguist-vendored: set
|
|
59
|
+
app/javascript/pages/home/index.tsx: linguist-generated: unspecified
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Side effects
|
|
63
|
+
|
|
64
|
+
`linguist-*` attributes are a GitHub convention, and plain git does not act on
|
|
65
|
+
them. Your local `git diff`, your merges, your CI and every checkout stay
|
|
66
|
+
byte-for-byte what they were. Away from GitHub's own pages, nothing changes.
|
|
67
|
+
|
|
68
|
+
On GitHub itself:
|
|
69
|
+
|
|
70
|
+
- **`linguist-generated` collapses the file in diffs.** Linguist's docs put it
|
|
71
|
+
plainly: these files "are suppressed in diffs". Reviewers get a "Load diff"
|
|
72
|
+
button instead of the patch. The gem uses this attribute only for tool
|
|
73
|
+
output; everything hand-written gets `linguist-vendored`, which leaves diffs
|
|
74
|
+
alone. That is why `--all-frontend` vendors your frontend rather than
|
|
75
|
+
marking it generated.
|
|
76
|
+
- **Code search still indexes the files.** They stay findable, and GitHub adds
|
|
77
|
+
`is:generated` and `is:vendored` filters, so a query written as
|
|
78
|
+
`-is:vendored` will skip them.
|
|
79
|
+
- **Syntax highlighting is untouched.** Highlighting follows
|
|
80
|
+
`linguist-language=`, which this gem never writes.
|
|
81
|
+
|
|
82
|
+
Nothing is deleted, moved or rewritten. The files stay in the repository and
|
|
83
|
+
in its history, and GitHub only reads `.gitattributes` once you commit it.
|
|
84
|
+
|
|
85
|
+
## A note on patterns
|
|
86
|
+
|
|
87
|
+
Directory rules emit `dir/**`, never `dir/*`. In gitignore syntax, which `.gitattributes` shares, a single star does not cross a slash, so `vendor/*` reaches `vendor/turbo.js` and never `vendor/javascript/turbo.js`. There is a spec that asks `git check-attr` rather than trusting the documentation.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT.
|
|
92
|
+
|
|
93
|
+
[irsk]: https://github.com/inertia-rails/react-starter-kit
|
|
94
|
+
[docs]: https://github.com/github-linguist/linguist/blob/main/docs/overrides.md
|
|
95
|
+
[demo]: https://github.com/irinanazarova/react-starter-kit-this-is-ruby
|
data/exe/this_is_ruby
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThisIsRuby
|
|
4
|
+
# Reads and rewrites .gitattributes, touching only the block it owns.
|
|
5
|
+
#
|
|
6
|
+
# Everything outside the markers is preserved: Rails writes its own lines
|
|
7
|
+
# there, and so do people. The block is rewritten where it already sits, so
|
|
8
|
+
# what someone put below it stays below it.
|
|
9
|
+
class AttributesFile
|
|
10
|
+
BEGIN_MARKER = "# --- this_is_ruby: begin ---"
|
|
11
|
+
END_MARKER = "# --- this_is_ruby: end ---"
|
|
12
|
+
BLOCK = /^#{Regexp.escape(BEGIN_MARKER)}\n.*?^#{Regexp.escape(END_MARKER)}\n?/m
|
|
13
|
+
|
|
14
|
+
PREAMBLE = [
|
|
15
|
+
"# Managed by this_is_ruby. Re-run it when your build setup changes.",
|
|
16
|
+
"# Every path below is excluded from this repository's language stats.",
|
|
17
|
+
"# Edit above or below this block, never inside it."
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :path
|
|
21
|
+
|
|
22
|
+
def initialize(path)
|
|
23
|
+
@path = Pathname.new(path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def current = path.file? ? path.read : ""
|
|
27
|
+
|
|
28
|
+
# Patterns already declared outside our block, as pattern => attributes.
|
|
29
|
+
#
|
|
30
|
+
# Attributes keep their `-` or `!` prefix: an owner who wrote
|
|
31
|
+
# `public/404.html -linguist-generated` has spoken about that path just as
|
|
32
|
+
# deliberately as one who set it, and Plan reads both as "leave it alone".
|
|
33
|
+
def existing_attributes
|
|
34
|
+
before, after = split
|
|
35
|
+
"#{before}#{after}".each_line.with_object({}) do |line, declared|
|
|
36
|
+
body = line.split("#", 2).first.to_s.strip
|
|
37
|
+
next if body.empty?
|
|
38
|
+
|
|
39
|
+
pattern, *attributes = body.split(/\s+/)
|
|
40
|
+
(declared[pattern] ||= []).concat(attributes)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render(emissions)
|
|
45
|
+
before, after = split
|
|
46
|
+
join(before, emissions.empty? ? nil : block(emissions), after)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def stale?(emissions) = render(emissions) != current
|
|
50
|
+
|
|
51
|
+
# Writes the file when the rendering differs, and says whether it did.
|
|
52
|
+
def write(emissions)
|
|
53
|
+
contents = render(emissions)
|
|
54
|
+
return false if contents == current
|
|
55
|
+
|
|
56
|
+
path.write(contents)
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# The file around our block. With no block, all of it is "before".
|
|
63
|
+
def split
|
|
64
|
+
match = BLOCK.match(current)
|
|
65
|
+
match ? [match.pre_match, match.post_match] : [current, ""]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def block(emissions)
|
|
69
|
+
body = "#{BEGIN_MARKER}\n"
|
|
70
|
+
PREAMBLE.each { |line| body << line << "\n" }
|
|
71
|
+
emissions.chunk_while { |a, b| a.rule == b.rule }.each do |group|
|
|
72
|
+
body << "\n# #{group.first.rule.summary}\n"
|
|
73
|
+
group.each { |emission| body << emission.line << "\n" }
|
|
74
|
+
end
|
|
75
|
+
body << END_MARKER << "\n"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def join(*sections)
|
|
79
|
+
sections.compact.map { |section| tidy(section) }.reject(&:empty?).join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# One trailing newline and no leading blank lines, so the sections join
|
|
83
|
+
# with exactly one blank line between them however the file arrived.
|
|
84
|
+
def tidy(text)
|
|
85
|
+
trimmed = text.sub(/\A\n+/, "").sub(/\n*\z/, "")
|
|
86
|
+
trimmed.empty? ? "" : "#{trimmed}\n"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module ThisIsRuby
|
|
6
|
+
# Three verbs: write the file, show what would be written, or fail when it
|
|
7
|
+
# has drifted. `check` is the one CI cares about.
|
|
8
|
+
class CLI
|
|
9
|
+
COMMANDS = %w[apply plan check].freeze
|
|
10
|
+
|
|
11
|
+
USAGE = <<~TEXT
|
|
12
|
+
Usage: this_is_ruby [command] [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
apply write the managed block into .gitattributes (default)
|
|
16
|
+
plan show what apply would write, change nothing
|
|
17
|
+
check exit 1 when .gitattributes is out of date (for CI)
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
TEXT
|
|
21
|
+
|
|
22
|
+
def self.call(argv, out: $stdout, err: $stderr)
|
|
23
|
+
new(argv, out:, err:).call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(argv, out: $stdout, err: $stderr)
|
|
27
|
+
@argv = argv
|
|
28
|
+
@out = out
|
|
29
|
+
@err = err
|
|
30
|
+
@options = {path: ".", all_frontend: false, color: out.tty?}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call
|
|
34
|
+
command = parse
|
|
35
|
+
# --help and --version leave nothing to run; bad input leaves nothing to
|
|
36
|
+
# run either, but must not look like success to a CI step.
|
|
37
|
+
return command if command.is_a?(Integer)
|
|
38
|
+
|
|
39
|
+
repo = Repo.at(@options[:path])
|
|
40
|
+
attributes = AttributesFile.new(repo.root.join(".gitattributes"))
|
|
41
|
+
plan = Plan.build(repo, attributes:, all_frontend: @options[:all_frontend])
|
|
42
|
+
report = Report.new(plan, out: @out, color: @options[:color])
|
|
43
|
+
|
|
44
|
+
send(command, plan, attributes, report)
|
|
45
|
+
rescue Repo::NotAGitRepo => error
|
|
46
|
+
@err.puts "this_is_ruby: #{error.message}"
|
|
47
|
+
1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def apply(plan, attributes, report)
|
|
53
|
+
report.call
|
|
54
|
+
report.wrote(display(attributes.path)) if attributes.write(plan.emissions)
|
|
55
|
+
0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def plan(plan, attributes, report)
|
|
59
|
+
report.call
|
|
60
|
+
0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check(plan, attributes, report)
|
|
64
|
+
return 0 unless attributes.stale?(plan.emissions)
|
|
65
|
+
|
|
66
|
+
report.call
|
|
67
|
+
report.drift(display(attributes.path))
|
|
68
|
+
1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def display(path)
|
|
72
|
+
path.relative_path_from(Pathname.pwd).to_s
|
|
73
|
+
rescue ArgumentError
|
|
74
|
+
path.to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse
|
|
78
|
+
parser = OptionParser.new do |opts|
|
|
79
|
+
opts.banner = USAGE
|
|
80
|
+
opts.on("--path DIR", "repository to inspect (default: .)") { |dir| @options[:path] = dir }
|
|
81
|
+
opts.on("--all-frontend", "also mark hand-written frontend sources as vendored") { @options[:all_frontend] = true }
|
|
82
|
+
opts.on("--[no-]color", "colourise output") { |on| @options[:color] = on }
|
|
83
|
+
opts.on("-v", "--version", "print the version") do
|
|
84
|
+
@out.puts VERSION
|
|
85
|
+
return 0
|
|
86
|
+
end
|
|
87
|
+
opts.on("-h", "--help", "print this message") do
|
|
88
|
+
@out.puts opts
|
|
89
|
+
return 0
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
rest = parser.parse(@argv)
|
|
93
|
+
return refuse(parser, "one command at a time, got #{rest.join(" ")}") if rest.size > 1
|
|
94
|
+
|
|
95
|
+
command = rest.first || "apply"
|
|
96
|
+
return refuse(parser, "unknown command #{command.inspect}") unless COMMANDS.include?(command)
|
|
97
|
+
|
|
98
|
+
command
|
|
99
|
+
rescue OptionParser::ParseError => error
|
|
100
|
+
refuse(parser, error.message)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns the exit status, so a typo in a CI step fails instead of passing.
|
|
104
|
+
def refuse(parser, message)
|
|
105
|
+
@err.puts "this_is_ruby: #{message}"
|
|
106
|
+
@err.puts parser
|
|
107
|
+
1
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThisIsRuby
|
|
4
|
+
# One line destined for .gitattributes, plus the tracked paths that earned
|
|
5
|
+
# it. The paths are what the report shows and what the language estimate
|
|
6
|
+
# subtracts; the pattern is all that reaches the file.
|
|
7
|
+
Emission = Data.define(:pattern, :attribute, :paths, :rule) do
|
|
8
|
+
def line
|
|
9
|
+
"#{pattern} #{attribute}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def bytes(repo)
|
|
13
|
+
paths.sum { |path| repo.size(path) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThisIsRuby
|
|
4
|
+
# A rough stand-in for GitHub's language bar.
|
|
5
|
+
#
|
|
6
|
+
# Linguist is far more careful than this: it sniffs content, resolves
|
|
7
|
+
# ambiguous extensions and applies heuristics we do not reimplement. The
|
|
8
|
+
# point here is only to show which way the bar moves, so the numbers are
|
|
9
|
+
# always presented as an estimate and GitHub stays the authority.
|
|
10
|
+
module LanguageEstimate
|
|
11
|
+
# Only types Linguist counts: programming and markup. Anything absent
|
|
12
|
+
# here (JSON, YAML, Markdown, lockfiles) is data or prose and scores
|
|
13
|
+
# nothing, which is why package-lock.json never mattered.
|
|
14
|
+
EXTENSIONS = {
|
|
15
|
+
".rb" => "Ruby", ".rake" => "Ruby", ".gemspec" => "Ruby", ".ru" => "Ruby",
|
|
16
|
+
".erb" => "HTML", ".html" => "HTML", ".htm" => "HTML",
|
|
17
|
+
".haml" => "Haml", ".slim" => "Slim",
|
|
18
|
+
".ts" => "TypeScript", ".tsx" => "TypeScript", ".mts" => "TypeScript",
|
|
19
|
+
".js" => "JavaScript", ".jsx" => "JavaScript", ".mjs" => "JavaScript",
|
|
20
|
+
".css" => "CSS", ".scss" => "SCSS", ".sass" => "SCSS", ".less" => "Less",
|
|
21
|
+
".vue" => "Vue", ".svelte" => "Svelte",
|
|
22
|
+
".py" => "Python", ".go" => "Go", ".rs" => "Rust", ".java" => "Java",
|
|
23
|
+
".sh" => "Shell", ".bash" => "Shell", ".sql" => "SQL", ".ejs" => "EJS"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
FILENAMES = {
|
|
27
|
+
"Gemfile" => "Ruby", "Rakefile" => "Ruby", "Guardfile" => "Ruby",
|
|
28
|
+
"Capfile" => "Ruby", "Dockerfile" => "Dockerfile"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# The parts of Linguist's own vendor list a Ruby project actually hits.
|
|
32
|
+
# Mirrored here so the estimate does not count bytes GitHub never counts.
|
|
33
|
+
VENDORED = Regexp.union(
|
|
34
|
+
%r{(\A|/)node_modules/},
|
|
35
|
+
%r{(\A|/)bower_components/},
|
|
36
|
+
%r{(\A|/)\.yarn/(releases|plugins|sdks|versions|unplugged)/},
|
|
37
|
+
%r{(\A|/)vendors?/},
|
|
38
|
+
/\.min\.(js|css)\z/
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# Returns [[language, bytes], ...], largest first.
|
|
44
|
+
def of(repo, excluding: Set.new)
|
|
45
|
+
totals = Hash.new(0)
|
|
46
|
+
repo.tracked.each do |path|
|
|
47
|
+
next if excluding.include?(path) || VENDORED.match?(path)
|
|
48
|
+
|
|
49
|
+
language = language_for(path)
|
|
50
|
+
totals[language] += repo.size(path) if language
|
|
51
|
+
end
|
|
52
|
+
totals.sort_by { |language, bytes| [-bytes, language] }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def language_for(path)
|
|
56
|
+
name = File.basename(path)
|
|
57
|
+
return FILENAMES[name] if FILENAMES.key?(name)
|
|
58
|
+
|
|
59
|
+
# `index.html.erb` is ERB templating HTML; GitHub reports it as HTML.
|
|
60
|
+
EXTENSIONS[File.extname(name).downcase]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThisIsRuby
|
|
4
|
+
# What we intend to write, and what we deliberately left alone.
|
|
5
|
+
class Plan
|
|
6
|
+
LINGUIST_ATTRIBUTES = %w[linguist-generated linguist-vendored linguist-documentation].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :repo, :emissions, :declared
|
|
9
|
+
|
|
10
|
+
def self.build(repo, attributes:, all_frontend: false)
|
|
11
|
+
spoken_for = spoken_for(attributes.existing_attributes)
|
|
12
|
+
found = Rules.for_level(all_frontend:).flat_map { |rule| rule.apply(repo) }
|
|
13
|
+
declared, emissions = found.partition do |emission|
|
|
14
|
+
spoken_for.any? { |pattern| covers?(pattern, emission.pattern) }
|
|
15
|
+
end
|
|
16
|
+
new(repo:, emissions: prune(emissions), declared:)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Patterns whose Linguist attributes the owner has already decided, set or
|
|
20
|
+
# unset. `-linguist-generated` is a decision too, and writing our own line
|
|
21
|
+
# after it would quietly reverse it, since the last match wins.
|
|
22
|
+
def self.spoken_for(existing)
|
|
23
|
+
existing.filter_map do |pattern, attributes|
|
|
24
|
+
names = attributes.map { |attribute| attribute.delete_prefix("-").delete_prefix("!").split("=").first }
|
|
25
|
+
pattern if (names & LINGUIST_ATTRIBUTES).any?
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Does a declared pattern speak for the one we were about to write?
|
|
30
|
+
#
|
|
31
|
+
# Only the two shapes we emit are resolved: `dir/**` reaches everything
|
|
32
|
+
# below it, and anything else matches the way a single star does in
|
|
33
|
+
# gitignore syntax. A declared pattern covering only some of an emission's
|
|
34
|
+
# paths is not detected.
|
|
35
|
+
def self.covers?(declared, pattern)
|
|
36
|
+
return true if declared == pattern
|
|
37
|
+
return pattern.start_with?(declared.delete_suffix("**")) if declared.end_with?("**")
|
|
38
|
+
|
|
39
|
+
File.fnmatch?(declared, pattern, File::FNM_PATHNAME)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Drops a pattern when a broader one already carries the same attribute:
|
|
43
|
+
# once `app/javascript/**` is vendored, saying so again about
|
|
44
|
+
# `app/javascript/components/ui/**` only makes the file longer.
|
|
45
|
+
def self.prune(emissions)
|
|
46
|
+
emissions.reject do |emission|
|
|
47
|
+
emissions.any? do |other|
|
|
48
|
+
next false unless other.pattern.end_with?("**")
|
|
49
|
+
next false if other.pattern == emission.pattern || other.attribute != emission.attribute
|
|
50
|
+
|
|
51
|
+
emission.pattern.start_with?(other.pattern.delete_suffix("**"))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(repo:, emissions:, declared:)
|
|
57
|
+
@repo = repo
|
|
58
|
+
@emissions = emissions.freeze
|
|
59
|
+
@declared = declared.freeze
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def empty? = emissions.empty?
|
|
63
|
+
|
|
64
|
+
def claimed_paths
|
|
65
|
+
@claimed_paths ||= emissions.flat_map(&:paths).to_set
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def before = @before ||= LanguageEstimate.of(repo)
|
|
69
|
+
|
|
70
|
+
def after = @after ||= LanguageEstimate.of(repo, excluding: claimed_paths)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module ThisIsRuby
|
|
7
|
+
# The git working tree under inspection.
|
|
8
|
+
#
|
|
9
|
+
# Only tracked files are considered. Linguist reads the repository, so a
|
|
10
|
+
# path git ignores is already invisible to it and needs no attribute.
|
|
11
|
+
# That is why a project that gitignores `coverage/`, as most do, gets
|
|
12
|
+
# nothing written about it.
|
|
13
|
+
class Repo
|
|
14
|
+
class NotAGitRepo < Error; end
|
|
15
|
+
|
|
16
|
+
# Reading a whole blob to look for a marker comment is wasteful, and the
|
|
17
|
+
# markers we check for all sit in the first few lines.
|
|
18
|
+
PEEK_BYTES = 4096
|
|
19
|
+
|
|
20
|
+
attr_reader :root
|
|
21
|
+
|
|
22
|
+
def self.at(path)
|
|
23
|
+
# capture3, not capture2e: git writes warnings and GIT_TRACE output to
|
|
24
|
+
# stderr while still succeeding, and merging them corrupts the path.
|
|
25
|
+
out, _err, status = Open3.capture3("git", "-C", path.to_s, "rev-parse", "--show-toplevel")
|
|
26
|
+
raise NotAGitRepo, "not a git repository: #{path}" unless status.success?
|
|
27
|
+
|
|
28
|
+
new(out.strip)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(root)
|
|
32
|
+
@root = Pathname.new(root).expand_path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns an Array of repo-relative paths, as git reports them.
|
|
36
|
+
def tracked
|
|
37
|
+
@tracked ||= begin
|
|
38
|
+
out, _err, status = Open3.capture3("git", "-C", root.to_s, "ls-files", "-z")
|
|
39
|
+
raise NotAGitRepo, "cannot list files in #{root}" unless status.success?
|
|
40
|
+
|
|
41
|
+
out.split("\0").reject(&:empty?).freeze
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def tracked?(path)
|
|
46
|
+
tracked_set.include?(path)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the first PEEK_BYTES of a tracked file, or nil when it is
|
|
50
|
+
# missing from the working tree (a sparse checkout, say).
|
|
51
|
+
def peek(path)
|
|
52
|
+
full = root.join(path)
|
|
53
|
+
return unless full.file?
|
|
54
|
+
|
|
55
|
+
full.open("rb") { |io| io.read(PEEK_BYTES) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def size(path)
|
|
59
|
+
full = root.join(path)
|
|
60
|
+
full.file? ? full.size : 0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def tracked_set
|
|
66
|
+
@tracked_set ||= tracked.to_set
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThisIsRuby
|
|
4
|
+
# Prints what the plan found. Everything here is presentation; decisions
|
|
5
|
+
# were made in Plan.
|
|
6
|
+
class Report
|
|
7
|
+
TOP = 5
|
|
8
|
+
|
|
9
|
+
def initialize(plan, out: $stdout, color: out.tty?)
|
|
10
|
+
@plan = plan
|
|
11
|
+
@out = out
|
|
12
|
+
@color = color
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
if @plan.empty? && @plan.declared.empty?
|
|
17
|
+
say "Nothing to do. Linguist already counts this repository fairly."
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
findings
|
|
22
|
+
declared
|
|
23
|
+
language_bar
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def wrote(path)
|
|
27
|
+
say "", "#{green("Wrote")} #{path}. Commit it: Linguist only reads what is in the repository."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def drift(path)
|
|
31
|
+
say "", "#{path} is out of date. Run #{bold("this_is_ruby")} and commit the result."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def findings
|
|
37
|
+
return if @plan.empty?
|
|
38
|
+
|
|
39
|
+
count = @plan.emissions.sum { |e| e.paths.size }
|
|
40
|
+
say "#{bold(count)} files will stop counting toward this repository's language bar:", ""
|
|
41
|
+
@plan.emissions.chunk_while { |a, b| a.rule == b.rule }.each do |group|
|
|
42
|
+
header = group.first.rule.summary
|
|
43
|
+
say " #{bold(header)} #{dim("#{human(group.sum { |e| e.bytes(@plan.repo) })}, #{group.sum { |e| e.paths.size }} files")}"
|
|
44
|
+
group.each { |emission| say " #{emission.line}" }
|
|
45
|
+
say ""
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def declared
|
|
50
|
+
return if @plan.declared.empty?
|
|
51
|
+
|
|
52
|
+
say dim("Already decided in .gitattributes, left alone:")
|
|
53
|
+
@plan.declared.each { |emission| say dim(" #{emission.pattern}") }
|
|
54
|
+
say ""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def language_bar
|
|
58
|
+
before = @plan.before
|
|
59
|
+
after = @plan.after
|
|
60
|
+
return if before.empty?
|
|
61
|
+
|
|
62
|
+
say "Estimated language bar #{dim("(GitHub is the authority; this is a guide)")}", ""
|
|
63
|
+
rows = (before.first(TOP).map(&:first) | after.first(TOP).map(&:first))
|
|
64
|
+
width = rows.map(&:length).max
|
|
65
|
+
say " #{"".ljust(width)} #{"now".rjust(7)} #{"after".rjust(7)}"
|
|
66
|
+
rows.each do |language|
|
|
67
|
+
say " #{language.ljust(width)} #{share(before, language).rjust(7)} #{share(after, language).rjust(7)}"
|
|
68
|
+
end
|
|
69
|
+
say ""
|
|
70
|
+
winner = after.first&.first
|
|
71
|
+
say " Primary language: #{bold(before.first.first)} #{dim("->")} #{green(winner)}" if winner && winner != before.first.first
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def share(totals, language)
|
|
75
|
+
total = totals.sum(&:last)
|
|
76
|
+
return "--" if total.zero?
|
|
77
|
+
|
|
78
|
+
bytes = totals.assoc(language)&.last.to_i
|
|
79
|
+
bytes.zero? ? "--" : format("%.1f%%", 100.0 * bytes / total)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def human(bytes)
|
|
83
|
+
units = ["B", "KB", "MB", "GB"]
|
|
84
|
+
unit = units.shift
|
|
85
|
+
value = bytes.to_f
|
|
86
|
+
while value >= 1024 && units.any?
|
|
87
|
+
value /= 1024
|
|
88
|
+
unit = units.shift
|
|
89
|
+
end
|
|
90
|
+
(unit == "B") ? "#{bytes} B" : format("%.1f %s", value, unit)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def say(*lines) = lines.each { |line| @out.puts(line) }
|
|
94
|
+
|
|
95
|
+
def bold(text) = @color ? "\e[1m#{text}\e[0m" : text
|
|
96
|
+
|
|
97
|
+
def dim(text) = @color ? "\e[2m#{text}\e[0m" : text
|
|
98
|
+
|
|
99
|
+
def green(text) = @color ? "\e[32m#{text}\e[0m" : text
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThisIsRuby
|
|
4
|
+
# One family of files that Linguist counts but nobody in the project wrote.
|
|
5
|
+
#
|
|
6
|
+
# A rule is handed the repo and returns a Hash of pattern => tracked paths.
|
|
7
|
+
# Rules never guess: when the evidence is not on disk they return nothing,
|
|
8
|
+
# so a project that has no coverage report gets no line about one.
|
|
9
|
+
#
|
|
10
|
+
# :safe rules find files a tool produced, and always run. :frontend rules
|
|
11
|
+
# find hand-written sources, and run only with --all-frontend, because
|
|
12
|
+
# excluding those is the owner's call about what their repository is rather
|
|
13
|
+
# than a fact about who typed the file.
|
|
14
|
+
class Rule
|
|
15
|
+
LEVELS = %i[safe frontend].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :key, :attribute, :summary, :level
|
|
18
|
+
|
|
19
|
+
def initialize(key:, attribute:, summary:, level: :safe, &finder)
|
|
20
|
+
raise ArgumentError, "unknown level #{level.inspect}" unless LEVELS.include?(level)
|
|
21
|
+
raise ArgumentError, "rule #{key} needs a finder" unless finder
|
|
22
|
+
|
|
23
|
+
@key = key
|
|
24
|
+
@attribute = attribute
|
|
25
|
+
@summary = summary
|
|
26
|
+
@level = level
|
|
27
|
+
@finder = finder
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def safe? = level == :safe
|
|
31
|
+
|
|
32
|
+
# Returns an Array of Emission, empty when this repo has nothing to claim.
|
|
33
|
+
def apply(repo)
|
|
34
|
+
found = @finder.call(repo) || {}
|
|
35
|
+
found.filter_map do |pattern, paths|
|
|
36
|
+
next if paths.nil? || paths.empty?
|
|
37
|
+
|
|
38
|
+
Emission.new(pattern:, attribute:, paths: paths.sort.freeze, rule: self)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThisIsRuby
|
|
4
|
+
# What this gem knows how to recognise.
|
|
5
|
+
#
|
|
6
|
+
# Nothing here duplicates Linguist's own defaults: `node_modules/`,
|
|
7
|
+
# `vendor/`, `.yarn/`, minified JavaScript and lockfiles are already
|
|
8
|
+
# excluded upstream, and repeating them would only make the file longer.
|
|
9
|
+
module Rules
|
|
10
|
+
# Written by `rails new` since Rails 3.0, each naming its own path in a
|
|
11
|
+
# comment. Rails 8.1 ships these lines itself; earlier versions, and any
|
|
12
|
+
# app generated with --skip-git, do not.
|
|
13
|
+
RAILS_ERROR_PAGES = %w[400 404 406-unsupported-browser 422 500].freeze
|
|
14
|
+
|
|
15
|
+
# SimpleCov's HTML formatter has opened its report with this title since
|
|
16
|
+
# 0.x. The directory is configurable, so we find it by the marker rather
|
|
17
|
+
# than by assuming `coverage/`.
|
|
18
|
+
SIMPLECOV_MARKER = "<title>Code coverage for"
|
|
19
|
+
|
|
20
|
+
# Where a Rails project keeps hand-written frontend sources, in the order
|
|
21
|
+
# the generators have used over the years.
|
|
22
|
+
FRONTEND_ROOTS = %w[app/javascript app/frontend app/webpack frontend].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Returns the tracked paths under `dir`, or an empty Array.
|
|
27
|
+
def under(repo, dir)
|
|
28
|
+
prefix = "#{dir}/"
|
|
29
|
+
repo.tracked.select { |path| path.start_with?(prefix) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A recursive pattern. `dir/*` would only reach the directory's immediate
|
|
33
|
+
# children. In gitignore syntax, which .gitattributes shares, a single
|
|
34
|
+
# star does not cross a slash.
|
|
35
|
+
def glob(dir) = "#{dir}/**"
|
|
36
|
+
|
|
37
|
+
def claim(repo, *dirs)
|
|
38
|
+
dirs.to_h { |dir| [glob(dir), under(repo, dir)] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
ALL = [
|
|
42
|
+
Rule.new(
|
|
43
|
+
key: :rails_error_pages,
|
|
44
|
+
attribute: "linguist-generated",
|
|
45
|
+
summary: "error pages written by `rails new`"
|
|
46
|
+
) do |repo|
|
|
47
|
+
RAILS_ERROR_PAGES.filter_map { |page|
|
|
48
|
+
path = "public/#{page}.html"
|
|
49
|
+
next unless repo.tracked?(path)
|
|
50
|
+
# Only the generated page says where it lives. A hand-written 404
|
|
51
|
+
# is the author's own work and stays counted.
|
|
52
|
+
next unless repo.peek(path)&.include?("<!-- This file lives in #{path} -->")
|
|
53
|
+
|
|
54
|
+
[path, [path]]
|
|
55
|
+
}.to_h
|
|
56
|
+
end,
|
|
57
|
+
|
|
58
|
+
Rule.new(
|
|
59
|
+
key: :bundled_assets,
|
|
60
|
+
attribute: "linguist-generated",
|
|
61
|
+
summary: "bundler output (jsbundling, cssbundling, dartsass, tailwind)"
|
|
62
|
+
) { |repo| claim(repo, "app/assets/builds") },
|
|
63
|
+
|
|
64
|
+
Rule.new(
|
|
65
|
+
key: :precompiled_assets,
|
|
66
|
+
attribute: "linguist-generated",
|
|
67
|
+
summary: "precompiled asset pipeline output"
|
|
68
|
+
) { |repo| claim(repo, "public/assets") },
|
|
69
|
+
|
|
70
|
+
Rule.new(
|
|
71
|
+
key: :webpacker_packs,
|
|
72
|
+
attribute: "linguist-generated",
|
|
73
|
+
summary: "Webpacker/Shakapacker output"
|
|
74
|
+
) { |repo| claim(repo, "public/packs", "public/packs-test") },
|
|
75
|
+
|
|
76
|
+
Rule.new(
|
|
77
|
+
key: :vite_build,
|
|
78
|
+
attribute: "linguist-generated",
|
|
79
|
+
summary: "vite_rails build output"
|
|
80
|
+
) { |repo| claim(repo, "public/vite", "public/vite-dev", "public/vite-test") },
|
|
81
|
+
|
|
82
|
+
Rule.new(
|
|
83
|
+
key: :coverage_report,
|
|
84
|
+
attribute: "linguist-generated",
|
|
85
|
+
summary: "SimpleCov HTML report"
|
|
86
|
+
) do |repo|
|
|
87
|
+
dirs = repo.tracked.filter_map { |path|
|
|
88
|
+
next unless path.end_with?("/index.html")
|
|
89
|
+
next unless repo.peek(path)&.include?(SIMPLECOV_MARKER)
|
|
90
|
+
|
|
91
|
+
File.dirname(path)
|
|
92
|
+
}.uniq
|
|
93
|
+
claim(repo, *dirs)
|
|
94
|
+
end,
|
|
95
|
+
|
|
96
|
+
Rule.new(
|
|
97
|
+
key: :sql_schema,
|
|
98
|
+
attribute: "linguist-generated",
|
|
99
|
+
summary: "SQL structure dump (Rails already marks db/schema.rb)"
|
|
100
|
+
) do |repo|
|
|
101
|
+
path = "db/structure.sql"
|
|
102
|
+
repo.tracked?(path) ? {path => [path]} : {}
|
|
103
|
+
end,
|
|
104
|
+
|
|
105
|
+
Rule.new(
|
|
106
|
+
key: :shadcn_components,
|
|
107
|
+
attribute: "linguist-vendored",
|
|
108
|
+
summary: "shadcn/ui components, copied in by its generator"
|
|
109
|
+
) do |repo|
|
|
110
|
+
# `components.json` is the generator's config; without it these are
|
|
111
|
+
# just ordinary components someone wrote.
|
|
112
|
+
next({}) unless repo.tracked?("components.json")
|
|
113
|
+
|
|
114
|
+
dirs = repo.tracked.filter_map { |path|
|
|
115
|
+
dir = File.dirname(path)
|
|
116
|
+
dir if dir.end_with?("components/ui") && path.match?(/\.(tsx|jsx|ts|js|vue|svelte)\z/)
|
|
117
|
+
}.uniq
|
|
118
|
+
claim(repo, *dirs)
|
|
119
|
+
end,
|
|
120
|
+
|
|
121
|
+
Rule.new(
|
|
122
|
+
key: :frontend_sources,
|
|
123
|
+
attribute: "linguist-vendored",
|
|
124
|
+
summary: "frontend sources, excluded at your request (--all-frontend)",
|
|
125
|
+
level: :frontend
|
|
126
|
+
) do |repo|
|
|
127
|
+
present = FRONTEND_ROOTS.select { |dir| under(repo, dir).any? }
|
|
128
|
+
claim(repo, *present)
|
|
129
|
+
end
|
|
130
|
+
].freeze
|
|
131
|
+
|
|
132
|
+
def for_level(all_frontend:)
|
|
133
|
+
all_frontend ? ALL : ALL.select(&:safe?)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/this_is_ruby.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "this_is_ruby/version"
|
|
4
|
+
|
|
5
|
+
# Tells GitHub Linguist which files in a Ruby project nobody actually wrote.
|
|
6
|
+
#
|
|
7
|
+
# GitHub picks a repository's language by counting bytes, and a Rails app
|
|
8
|
+
# carries a lot of bytes it never authored: generated error pages, compiled
|
|
9
|
+
# assets, coverage reports, components copied in by a frontend generator.
|
|
10
|
+
# Counted together they routinely outweigh the app's own Ruby, and the repo
|
|
11
|
+
# is labelled HTML or TypeScript.
|
|
12
|
+
#
|
|
13
|
+
# Linguist already has the fix, `.gitattributes` overrides, but nobody
|
|
14
|
+
# maintains that file by hand. This gem writes it from what is actually on
|
|
15
|
+
# disk.
|
|
16
|
+
module ThisIsRuby
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require_relative "this_is_ruby/repo"
|
|
21
|
+
require_relative "this_is_ruby/emission"
|
|
22
|
+
require_relative "this_is_ruby/rule"
|
|
23
|
+
require_relative "this_is_ruby/rules"
|
|
24
|
+
require_relative "this_is_ruby/plan"
|
|
25
|
+
require_relative "this_is_ruby/attributes_file"
|
|
26
|
+
require_relative "this_is_ruby/language_estimate"
|
|
27
|
+
require_relative "this_is_ruby/report"
|
|
28
|
+
require_relative "this_is_ruby/cli"
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: this_is_ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Irina Nazarova
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
GitHub picks a repository's language by counting bytes, and a Rails app
|
|
14
|
+
carries plenty it never authored: generated error pages, compiled assets,
|
|
15
|
+
coverage reports, components copied in by a frontend generator. Together
|
|
16
|
+
they routinely outweigh the app's own Ruby. this_is_ruby finds them and
|
|
17
|
+
writes the .gitattributes overrides Linguist already understands.
|
|
18
|
+
email:
|
|
19
|
+
- inazarova@evilmartians.com
|
|
20
|
+
executables:
|
|
21
|
+
- this_is_ruby
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- LICENSE.txt
|
|
27
|
+
- README.md
|
|
28
|
+
- exe/this_is_ruby
|
|
29
|
+
- lib/this_is_ruby.rb
|
|
30
|
+
- lib/this_is_ruby/attributes_file.rb
|
|
31
|
+
- lib/this_is_ruby/cli.rb
|
|
32
|
+
- lib/this_is_ruby/emission.rb
|
|
33
|
+
- lib/this_is_ruby/language_estimate.rb
|
|
34
|
+
- lib/this_is_ruby/plan.rb
|
|
35
|
+
- lib/this_is_ruby/repo.rb
|
|
36
|
+
- lib/this_is_ruby/report.rb
|
|
37
|
+
- lib/this_is_ruby/rule.rb
|
|
38
|
+
- lib/this_is_ruby/rules.rb
|
|
39
|
+
- lib/this_is_ruby/version.rb
|
|
40
|
+
homepage: https://github.com/evilmartians/this_is_ruby
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata:
|
|
44
|
+
source_code_uri: https://github.com/evilmartians/this_is_ruby
|
|
45
|
+
changelog_uri: https://github.com/evilmartians/this_is_ruby/blob/main/CHANGELOG.md
|
|
46
|
+
bug_tracker_uri: https://github.com/evilmartians/this_is_ruby/issues
|
|
47
|
+
rubygems_mfa_required: 'true'
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.2.0
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.6.7
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Tell GitHub which files in your Ruby project nobody wrote
|
|
65
|
+
test_files: []
|