[種類, 色](色はLinguistと同じ、MaronとSlangは独自) */ private const LANGS = [ 'Ada' => ['p', '#02f88c'], 'Assembly' => ['p', '#6E4C13'], 'AWK' => ['p', '#c30e9b'], 'Batchfile' => ['p', '#C1F12E'], 'C' => ['p', '#555555'], 'C#' => ['p', '#178600'], 'C++' => ['p', '#f34b7d'], 'Clojure' => ['p', '#db5855'], 'CMake' => ['p', '#DA3434'], 'Common Lisp' => ['p', '#3fb68b'], 'Coq' => ['p', '#d0b68c'], 'Crystal' => ['p', '#000100'], 'CSS' => ['m', '#663399'], 'CUDA' => ['p', '#3A4E3A'], 'Cython' => ['p', '#fedf5b'], 'D' => ['p', '#ba595e'], 'Dart' => ['p', '#00B4AB'], 'Dockerfile' => ['p', '#384d54'], 'Elixir' => ['p', '#6e4a7e'], 'Elm' => ['p', '#60B5CC'], 'Emacs Lisp' => ['p', '#c065db'], 'Erlang' => ['p', '#B83998'], 'F#' => ['p', '#b845fc'], 'Forth' => ['p', '#341708'], 'Fortran' => ['p', '#4d41b1'], 'GDScript' => ['p', '#355570'], 'Gleam' => ['p', '#ffaff3'], 'GLSL' => ['p', '#5686a5'], 'Go' => ['p', '#00ADD8'], 'Groovy' => ['p', '#4298b8'], 'Handlebars' => ['m', '#f7931e'], 'Hare' => ['p', '#9d7424'], 'Haskell' => ['p', '#5e5086'], 'Haxe' => ['p', '#df7900'], 'HCL' => ['p', '#844FBA'], 'HLSL' => ['p', '#aace60'], 'HTML' => ['m', '#e34c26'], 'Java' => ['p', '#b07219'], 'JavaScript' => ['p', '#f1e05a'], 'Julia' => ['p', '#a270ba'], 'Jupyter Notebook' => ['m', '#DA5B0B'], 'Just' => ['p', '#384d54'], 'Kotlin' => ['p', '#A97BFF'], 'Less' => ['m', '#1d365d'], 'Lex' => ['p', '#DBCA00'], 'Lua' => ['p', '#000080'], 'Makefile' => ['p', '#427819'], 'Maron' => ['m', '#ea79d8'], 'MATLAB' => ['p', '#e16737'], 'Meson' => ['p', '#007800'], 'Metal' => ['p', '#8f14e9'], 'Nim' => ['p', '#ffc200'], 'Nix' => ['p', '#7e7eff'], 'Objective-C' => ['p', '#438eff'], 'Objective-C++' => ['p', '#6866fb'], 'OCaml' => ['p', '#ef7a08'], 'Odin' => ['p', '#60AFFE'], 'Pascal' => ['p', '#E3F171'], 'Perl' => ['p', '#0298c3'], 'PHP' => ['p', '#4F5D95'], 'PowerShell' => ['p', '#012456'], 'Python' => ['p', '#3572A5'], 'QML' => ['p', '#44a51c'], 'R' => ['p', '#198CE7'], 'Racket' => ['p', '#3c5caa'], 'Raku' => ['p', '#0000fb'], 'Roff' => ['m', '#ecdebe'], 'Ruby' => ['p', '#701516'], 'Rust' => ['p', '#dea584'], 'Sass' => ['m', '#a53b70'], 'Scala' => ['p', '#c22d40'], 'Scheme' => ['p', '#1e4aec'], 'SCSS' => ['m', '#c6538c'], 'Shell' => ['p', '#89e051'], 'Slang' => ['p', '#1fbec9'], 'Solidity' => ['p', '#AA6746'], 'Starlark' => ['p', '#76d275'], 'Svelte' => ['m', '#ff3e00'], 'Swift' => ['p', '#F05138'], 'SystemVerilog' => ['p', '#DAE1C2'], 'Tcl' => ['p', '#e4cc98'], 'TeX' => ['m', '#3D6117'], 'TSX' => ['p', '#3178c6'], 'Twig' => ['m', '#c1d026'], 'TypeScript' => ['p', '#3178c6'], 'V' => ['p', '#4f87c4'], 'Verilog' => ['p', '#b2b7f8'], 'VHDL' => ['p', '#adb2cb'], 'Vim Script' => ['p', '#199f4b'], 'Visual Basic .NET' => ['p', '#945db7'], 'Vue' => ['m', '#41b883'], 'WGSL' => ['p', '#1a5e9a'], 'Yacc' => ['p', '#4B6C4B'], 'Zig' => ['p', '#ec915c'], // 数えない(linguist-detectableで数えられる) 'JSON' => ['d', '#292929'], 'Markdown' => ['t', '#083fa1'], 'SQL' => ['d', '#e38c00'], 'TOML' => ['d', '#9c4221'], 'XML' => ['d', '#0060ac'], 'YAML' => ['d', '#cb171e'], ]; /** 拡張子(小文字) => 言語 */ private const EXTENSIONS = [ 'adb' => 'Ada', 'ads' => 'Ada', 'asm' => 'Assembly', 'nasm' => 'Assembly', 's' => 'Assembly', 'awk' => 'AWK', 'bat' => 'Batchfile', 'cmd' => 'Batchfile', 'c' => 'C', 'cs' => 'C#', 'csx' => 'C#', 'cpp' => 'C++', 'cc' => 'C++', 'cxx' => 'C++', 'c++' => 'C++', 'hpp' => 'C++', 'hh' => 'C++', 'hxx' => 'C++', 'h++' => 'C++', 'ipp' => 'C++', 'tpp' => 'C++', 'inl' => 'C++', 'ixx' => 'C++', 'cppm' => 'C++', 'clj' => 'Clojure', 'cljs' => 'Clojure', 'cljc' => 'Clojure', 'cmake' => 'CMake', 'lisp' => 'Common Lisp', 'lsp' => 'Common Lisp', 'cr' => 'Crystal', 'css' => 'CSS', 'cu' => 'CUDA', 'cuh' => 'CUDA', 'pyx' => 'Cython', 'pxd' => 'Cython', 'dart' => 'Dart', 'dockerfile' => 'Dockerfile', 'ex' => 'Elixir', 'exs' => 'Elixir', 'elm' => 'Elm', 'el' => 'Emacs Lisp', 'erl' => 'Erlang', 'hrl' => 'Erlang', 'fsi' => 'F#', 'fsx' => 'F#', 'fth' => 'Forth', '4th' => 'Forth', 'forth' => 'Forth', 'f' => 'Fortran', 'for' => 'Fortran', 'f77' => 'Fortran', 'f90' => 'Fortran', 'f95' => 'Fortran', 'f03' => 'Fortran', 'f08' => 'Fortran', 'gd' => 'GDScript', 'gleam' => 'Gleam', 'glsl' => 'GLSL', 'vert' => 'GLSL', 'frag' => 'GLSL', 'geom' => 'GLSL', 'comp' => 'GLSL', 'tesc' => 'GLSL', 'tese' => 'GLSL', 'rgen' => 'GLSL', 'rchit' => 'GLSL', 'rmiss' => 'GLSL', 'rahit' => 'GLSL', 'mesh' => 'GLSL', 'task' => 'GLSL', 'go' => 'Go', 'groovy' => 'Groovy', 'gradle' => 'Groovy', 'hbs' => 'Handlebars', 'handlebars' => 'Handlebars', 'ha' => 'Hare', 'hs' => 'Haskell', 'lhs' => 'Haskell', 'hx' => 'Haxe', 'tf' => 'HCL', 'hcl' => 'HCL', 'hlsl' => 'HLSL', 'hlsli' => 'HLSL', 'fx' => 'HLSL', 'fxh' => 'HLSL', 'html' => 'HTML', 'htm' => 'HTML', 'xhtml' => 'HTML', 'java' => 'Java', 'js' => 'JavaScript', 'mjs' => 'JavaScript', 'cjs' => 'JavaScript', 'jsx' => 'JavaScript', 'jl' => 'Julia', 'ipynb' => 'Jupyter Notebook', 'just' => 'Just', 'kt' => 'Kotlin', 'kts' => 'Kotlin', 'less' => 'Less', 'l' => 'Lex', 'lex' => 'Lex', 'lua' => 'Lua', 'mk' => 'Makefile', 'mak' => 'Makefile', 'maron' => 'Maron', 'metal' => 'Metal', 'nim' => 'Nim', 'nims' => 'Nim', 'nimble' => 'Nim', 'nix' => 'Nix', 'mm' => 'Objective-C++', 'ml' => 'OCaml', 'mli' => 'OCaml', 'odin' => 'Odin', 'pas' => 'Pascal', 'pp' => 'Pascal', 'dpr' => 'Pascal', 'lpr' => 'Pascal', 'pl' => 'Perl', 'pm' => 'Perl', 'php' => 'PHP', 'phtml' => 'PHP', 'php3' => 'PHP', 'php4' => 'PHP', 'php5' => 'PHP', 'phps' => 'PHP', 'ps1' => 'PowerShell', 'psm1' => 'PowerShell', 'psd1' => 'PowerShell', 'py' => 'Python', 'pyw' => 'Python', 'pyi' => 'Python', 'qml' => 'QML', 'r' => 'R', 'rkt' => 'Racket', 'raku' => 'Raku', 'rakumod' => 'Raku', 'p6' => 'Raku', 'roff' => 'Roff', 'man' => 'Roff', 'mdoc' => 'Roff', '1' => 'Roff', '2' => 'Roff', '3' => 'Roff', '4' => 'Roff', '5' => 'Roff', '6' => 'Roff', '7' => 'Roff', '8' => 'Roff', '9' => 'Roff', 'rb' => 'Ruby', 'rake' => 'Ruby', 'gemspec' => 'Ruby', 'rs' => 'Rust', 'sass' => 'Sass', 'scala' => 'Scala', 'scm' => 'Scheme', 'ss' => 'Scheme', 'scss' => 'SCSS', 'sh' => 'Shell', 'bash' => 'Shell', 'zsh' => 'Shell', 'ksh' => 'Shell', 'slang' => 'Slang', 'sol' => 'Solidity', 'bzl' => 'Starlark', 'star' => 'Starlark', 'svelte' => 'Svelte', 'swift' => 'Swift', 'sv' => 'SystemVerilog', 'svh' => 'SystemVerilog', 'tcl' => 'Tcl', 'tex' => 'TeX', 'sty' => 'TeX', 'ltx' => 'TeX', 'tsx' => 'TSX', 'twig' => 'Twig', 'ts' => 'TypeScript', 'mts' => 'TypeScript', 'cts' => 'TypeScript', 'vhd' => 'VHDL', 'vhdl' => 'VHDL', 'vim' => 'Vim Script', 'vb' => 'Visual Basic .NET', 'vue' => 'Vue', 'wgsl' => 'WGSL', 'y' => 'Yacc', 'yy' => 'Yacc', 'zig' => 'Zig', 'zon' => 'Zig', 'json' => 'JSON', 'md' => 'Markdown', 'markdown' => 'Markdown', 'sql' => 'SQL', 'toml' => 'TOML', 'xml' => 'XML', 'yml' => 'YAML', 'yaml' => 'YAML', ]; /** 拡張子で決まらない物(中身で決める、決まらなければこの言語) */ private const AMBIGUOUS = ['h' => 'C', 'm' => 'Objective-C', 'v' => 'V', 'fs' => 'F#', 'd' => 'D']; /** ファイル名 => 言語 */ private const FILENAMES = [ 'Makefile' => 'Makefile', 'makefile' => 'Makefile', 'GNUmakefile' => 'Makefile', 'BSDmakefile' => 'Makefile', 'Kbuild' => 'Makefile', 'Makefile.am' => 'Makefile', 'CMakeLists.txt' => 'CMake', 'meson.build' => 'Meson', 'meson_options.txt' => 'Meson', 'meson.options' => 'Meson', 'Dockerfile' => 'Dockerfile', 'Containerfile' => 'Dockerfile', 'Rakefile' => 'Ruby', 'Gemfile' => 'Ruby', 'Vagrantfile' => 'Ruby', 'Podfile' => 'Ruby', 'Justfile' => 'Just', 'justfile' => 'Just', '.justfile' => 'Just', 'BUILD' => 'Starlark', 'BUILD.bazel' => 'Starlark', 'WORKSPACE' => 'Starlark', 'WORKSPACE.bazel' => 'Starlark', 'MODULE.bazel' => 'Starlark', '.bashrc' => 'Shell', '.bash_profile' => 'Shell', '.bash_logout' => 'Shell', '.profile' => 'Shell', '.zshrc' => 'Shell', '.zprofile' => 'Shell', '.kshrc' => 'Shell', '.xinitrc' => 'Shell', '.xsession' => 'Shell', 'PKGBUILD' => 'Shell', 'APKBUILD' => 'Shell', '.vimrc' => 'Vim Script', '_vimrc' => 'Vim Script', '.gvimrc' => 'Vim Script', '.emacs' => 'Emacs Lisp', 'SConstruct' => 'Python', 'SConscript' => 'Python', 'Jenkinsfile' => 'Groovy', ]; /** #!のコマンド(後ろの数字を除く) => 言語 */ private const INTERPRETERS = [ 'sh' => 'Shell', 'bash' => 'Shell', 'zsh' => 'Shell', 'ksh' => 'Shell', 'mksh' => 'Shell', 'oksh' => 'Shell', 'pdksh' => 'Shell', 'dash' => 'Shell', 'ash' => 'Shell', 'rc' => 'Shell', 'fish' => 'Shell', 'python' => 'Python', 'pypy' => 'Python', 'ruby' => 'Ruby', 'jruby' => 'Ruby', 'perl' => 'Perl', 'raku' => 'Raku', 'php' => 'PHP', 'node' => 'JavaScript', 'nodejs' => 'JavaScript', 'bun' => 'JavaScript', 'deno' => 'TypeScript', 'lua' => 'Lua', 'luajit' => 'Lua', 'tclsh' => 'Tcl', 'wish' => 'Tcl', 'awk' => 'AWK', 'gawk' => 'AWK', 'nawk' => 'AWK', 'mawk' => 'AWK', 'rscript' => 'R', 'make' => 'Makefile', 'gmake' => 'Makefile', 'bmake' => 'Makefile', 'julia' => 'Julia', 'elixir' => 'Elixir', 'escript' => 'Erlang', 'guile' => 'Scheme', 'csi' => 'Scheme', 'racket' => 'Racket', 'sbcl' => 'Common Lisp', 'clisp' => 'Common Lisp', 'ecl' => 'Common Lisp', 'ocaml' => 'OCaml', 'runghc' => 'Haskell', 'runhaskell' => 'Haskell', 'crystal' => 'Crystal', 'pwsh' => 'PowerShell', 'dart' => 'Dart', 'swift' => 'Swift', 'groovy' => 'Groovy', 'scala' => 'Scala', 'emacs' => 'Emacs Lisp', ]; /** linguist-languageの別名(小文字) */ private const ALIASES = [ 'cpp' => 'C++', 'csharp' => 'C#', 'js' => 'JavaScript', 'ts' => 'TypeScript', 'sh' => 'Shell', 'bash' => 'Shell', 'golang' => 'Go', 'py' => 'Python', 'rb' => 'Ruby', 'objc' => 'Objective-C', 'objcpp' => 'Objective-C++', 'asm' => 'Assembly', 'make' => 'Makefile', 'vim' => 'Vim Script', 'viml' => 'Vim Script', 'elisp' => 'Emacs Lisp', 'md' => 'Markdown', 'yml' => 'YAML', 'vbnet' => 'Visual Basic .NET', 'terraform' => 'HCL', 'latex' => 'TeX', 'groff' => 'Roff', 'nroff' => 'Roff', 'manpage' => 'Roff', ]; /** 外部のコード(Linguistのvendor.ymlの主な物) */ private const VENDORED = '~(^|/)(vendor|vendors|node_modules|bower_components|jspm_packages|third[_-]?party|3rd[_-]?party|externals?|extern|deps|' .'Pods|Carthage|Godeps|\.git|\.svn|\.hg|\.vscode|\.idea|\.yarn|\.bundle)/' .'|(^|/)(gradlew(\.bat)?|mvnw(\.cmd)?|config\.(guess|sub|status)|ltmain\.sh|install-sh|missing|depcomp|aclocal\.m4|Makefile\.in)$' .'|\.min\.(js|css)$|-min\.js$|(^|/)(jquery|bootstrap)[^/]*\.(js|css)$~i'; /** ドキュメント */ private const DOCUMENTATION = '~^(docs?|documentation|man|examples?|samples?|demos?)/|(^|/)(javadoc|groovydoc)/' .'|(^|/)(README|CHANGELOG|CHANGES|LICEN[CS]E|COPYING|CONTRIBUTING|INSTALL|NEWS|AUTHORS|THANKS|HISTORY)(\.[^/]*)?$~i'; /** 生成されたファイル */ private const GENERATED = '~(\.pb\.(go|cc|h)|_pb2\.py|\.designer\.cs|\.g\.dart|\.freezed\.dart)$' .'|(^|/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|composer\.lock|go\.sum)$~'; /** 言語の色(知らない言語は灰色) */ public static function color(string $name): string { return self::LANGS[$name][1] ?? '#cccccc'; } /** * 言語毎のバイト数(保存した物が有ればそれ) * * @param bool $head 既定のブランチ(リポジトリの一覧に出す言語) * @return array|null 言語 => バイト数(多い順)。ファイルが多過ぎればnull */ public static function get(Repository $repo, string $treeish, bool $head = false): ?array { $tree = $repo->treeOf($treeish); if ($tree === null) return null; $file = $repo->path.'/gitler-languages.json'; $c = self::load($file); if (isset($c['trees'][$tree])) { if ($head && $c['head'] !== $tree) { $c['head'] = $tree; self::save($repo, $file, $c); } return $c['trees'][$tree]; } $bytes = self::stats($repo, $tree); if ($bytes === null) return null; $c['trees'][$tree] = $bytes; if ($head) $c['head'] = $tree; // 古い物から消す(既定のブランチの物は残す) foreach (array_keys($c['trees']) as $k) { if (count($c['trees']) <= self::KEEP) break; if ($k !== $c['head'] && $k !== $tree) unset($c['trees'][$k]); } self::save($repo, $file, $c); return $bytes; } /** * 既定のブランチの一番多い言語(保存した物だけ見る) * * @param string $dir リポジトリのディレクトリ * @return array|null name、color */ public static function primary(string $dir): ?array { $c = self::load($dir.'/gitler-languages.json'); $name = array_key_first($c['trees'][$c['head']] ?? []); return $name === null ? null : ['name' => (string)$name, 'color' => self::color((string)$name)]; } /** * 表示用(多い順、多過ぎれば残りは「その他」) * * @return array name、color、percent(「12.3%」)、width(CSSの幅) */ public static function present(array $bytes): array { $total = array_sum($bytes); if ($total <= 0) return []; arsort($bytes); if (count($bytes) > self::SHOW) { $rest = array_slice($bytes, self::SHOW - 1, null, true); $bytes = array_slice($bytes, 0, self::SHOW - 1, true) + ['その他' => array_sum($rest)]; } $out = []; foreach ($bytes as $name => $n) { $p = $n * 100 / $total; $out[] = ['name' => (string)$name, 'color' => $name === 'その他' ? '#746c75' : self::color((string)$name), 'percent' => $p < 0.1 ? '<0.1%' : number_format($p, 1).'%', 'width' => sprintf('%.3F', $p)]; } return $out; } /** * ツリーの言語毎のバイト数を数える * * @return array|null 言語 => バイト数(多い順)。ファイルが多過ぎればnull */ public static function stats(Repository $repo, string $treeish): ?array { $root = $repo->treeOf($treeish); if ($root === null) return null; $rules = self::attributes($repo, $root); // 除外を取り消す.gitattributesが無ければ、除外するディレクトリの中は見ない $undo = false; foreach ($rules as [, $set]) { foreach (['linguist-vendored', 'linguist-generated', 'linguist-documentation'] as $k) if (array_key_exists($k, $set) && $set[$k] !== true) $undo = true; } $bytes = []; $files = 0; $stack = [['', $root]]; while (!empty($stack)) { [$dir, $id] = array_pop($stack); foreach ($repo->tree($id)->entries as [$mode, $name, $eid]) { $path = $dir.$name; if ($mode === Tree::DIR) { if ($undo || !preg_match(self::VENDORED, $path.'/') && !preg_match(self::DOCUMENTATION, $path.'/')) $stack[] = [$path.'/', $eid]; continue; } // リンクとサブモジュールは数えない if ($mode !== Tree::FILE && $mode !== Tree::EXEC) continue; if (++$files > self::MAX_FILES) return null; $size = 0; $lang = self::file($repo, $path, $mode, $eid, self::match($rules, $path), $size); if ($lang !== null) $bytes[$lang] = ($bytes[$lang] ?? 0) + $size; } } arsort($bytes); return $bytes; } /** * 1つのファイルの言語(数えなければnull) * * @param array $a .gitattributesの属性 */ private static function file(Repository $repo, string $path, string $mode, string $id, array $a, int &$size): ?string { if ($a['linguist-vendored'] ?? (bool)preg_match(self::VENDORED, $path)) return null; if ($a['linguist-generated'] ?? (bool)preg_match(self::GENERATED, $path)) return null; if ($a['linguist-documentation'] ?? (bool)preg_match(self::DOCUMENTATION, $path)) return null; $size = (int)($repo->objects->info($id)[1] ?? 0); $base = basename($path); $dot = strrpos($base, '.'); $ext = $dot === false || $dot === 0 ? '' : strtolower(substr($base, $dot + 1)); $lang = isset($a['linguist-language']) ? self::named((string)$a['linguist-language']) : null; $lang ??= self::FILENAMES[$base] ?? null; if ($lang === null && preg_match('/^((GNU|BSD)?[Mm]akefile|Dockerfile|Containerfile)\./', $base, $m)) $lang = str_ends_with($m[1], 'akefile') ? 'Makefile' : 'Dockerfile'; $lang ??= self::EXTENSIONS[$ext] ?? null; // 中身を見る:拡張子で決まらない物と、拡張子の無いスクリプト if ($lang === null && (isset(self::AMBIGUOUS[$ext]) || $ext === '' || $ext === 'cgi' || $mode === Tree::EXEC)) { if ($size > self::MAX_PEEK) { $lang = self::AMBIGUOUS[$ext] ?? null; } else { $data = $repo->blob($id); if (Diff::isBinary($data)) return null; // Autoconfのconfigure等は生成された物 if (str_contains(substr($data, 0, 4096), 'Generated by GNU Autoconf')) return null; $lang = isset(self::AMBIGUOUS[$ext]) ? self::guess($ext, $data) : self::shebang($data); } } if ($lang === null) return null; $type = self::LANGS[$lang][0] ?? self::PROGRAMMING; return ($a['linguist-detectable'] ?? ($type === self::PROGRAMMING || $type === self::MARKUP)) ? $lang : null; } /** linguist-languageの名前(大文字・小文字、空白・「-」・「_」は区別しない) */ private static function named(string $value): string { $key = strtolower($value); if (isset(self::ALIASES[$key])) return self::ALIASES[$key]; $norm = fn(string $s): string => str_replace([' ', '-', '_'], '', strtolower($s)); foreach (array_keys(self::LANGS) as $name) if ($norm($name) === $norm($value)) return $name; return $value; } /** 拡張子で決まらない物を中身で決める */ private static function guess(string $ext, string $data): ?string { return match ($ext) { 'h' => preg_match('/^\s*(@interface|@implementation|@protocol|@end|#import)\b/m', $data) ? 'Objective-C' : (preg_match('/^\s*(class|namespace|template\s*<|using\s+namespace)\b|\bstd::|^\s*(public|private|protected)\s*:|#include\s*<(iostream|string|vector|map|memory|algorithm|cstd\w+)>/m', $data) ? 'C++' : 'C'), 'm' => preg_match('/^\s*(@interface|@implementation|@protocol|@end|#import|#include)\b/m', $data) ? 'Objective-C' : (preg_match('/^\s*(function\b|%)/m', $data) ? 'MATLAB' : 'Objective-C'), 'v' => preg_match('/^\s*module\s+\w+[\s\S]*^\s*endmodule\b/m', $data) ? 'Verilog' : (preg_match('/^\s*(Theorem|Lemma|Proof|Qed|Require\s+Import|Inductive)\b/m', $data) ? 'Coq' : 'V'), 'fs' => preg_match('/^\s*#version\s+\d+|\bgl_Frag\w+|^\s*(uniform|in|out)\s+(vec|float|int|mat|sampler)\w*\s+\w+\s*;/m', $data) ? 'GLSL' : 'F#', // makeの依存関係のファイル(生成された物) 'd' => preg_match('/^[^\s:#]+\.o\s*:/m', $data) ? null : 'D', default => self::AMBIGUOUS[$ext] ?? null, }; } /** #!から言語を決める(「#!/usr/bin/env -S php」等) */ private static function shebang(string $data): ?string { if (!str_starts_with($data, '#!')) return null; $words = preg_split('/\s+/', trim(strtok(substr($data, 2), "\n"))); $prog = basename((string)array_shift($words)); if ($prog === 'env') { $prog = ''; foreach ($words as $w) { if ($w === '' || $w[0] === '-' || str_contains($w, '=')) continue; $prog = basename($w); break; } } $prog = (string)preg_replace('/[\d.\-]+$/', '', strtolower($prog)); return self::INTERPRETERS[$prog] ?? null; } //////// .gitattributes /** * ルートの.gitattributesのlinguist-*の属性 * * @return array [正規表現, 属性 => true、false、null(未指定)又は言語]の配列 */ private static function attributes(Repository $repo, string $root): array { $e = $repo->tree($root)->find('.gitattributes'); if ($e === null || ($e[0] !== Tree::FILE && $e[0] !== Tree::EXEC)) return []; $rules = []; foreach (preg_split('/\r?\n/', $repo->blob($e[2])) as $line) { $line = trim($line); if ($line === '' || $line[0] === '#') continue; $words = preg_split('/\s+/', $line); $pattern = (string)array_shift($words); $set = []; foreach ($words as $w) { if (!preg_match('/^([-!]?)(linguist-[a-z]+)(?:=(.*))?$/', $w, $m)) continue; if ($m[1] === '!') $set[$m[2]] = null; else if ($m[2] === 'linguist-language') $set[$m[2]] = $m[1] === '' && ($m[3] ?? '') !== '' ? $m[3] : null; else $set[$m[2]] = $m[1] === '-' ? false : !in_array(strtolower($m[3] ?? 'true'), ['false', '0', 'no'], true); } if (!empty($set)) $rules[] = [self::glob($pattern), $set]; } return $rules; } /** 後に書いた物が優先 */ private static function match(array $rules, string $path): array { $a = []; foreach ($rules as [$re, $set]) if (preg_match($re, $path)) $a = array_merge($a, $set); return $a; } /** .gitattributesのパターンを正規表現にする(「/」が無ければどの深さの名前にも一致する) */ public static function glob(string $pattern): string { // ディレクトリだけのパターンはファイルに一致しない if (str_ends_with($pattern, '/')) return '~(?!)~'; $anchored = str_contains($pattern, '/'); if (str_starts_with($pattern, '/')) $pattern = substr($pattern, 1); $re = ''; $len = strlen($pattern); for ($i = 0; $i < $len; $i++) { $ch = $pattern[$i]; $start = $i === 0 || $pattern[$i - 1] === '/'; if ($ch === '*' && $start && substr($pattern, $i, 3) === '**/') { $re .= '(?:.*/)?'; $i += 2; } else if ($ch === '*' && $start && $i + 2 === $len && substr($pattern, $i, 2) === '**') { $re .= '.*'; $i++; } else if ($ch === '*') { $re .= '[^/]*'; } else if ($ch === '?') { $re .= '[^/]'; } else if ($ch === '\\' && $i + 1 < $len) { $re .= preg_quote($pattern[++$i], '~'); } else if ($ch === '[' && ($end = strpos($pattern, ']', $i + 2)) !== false) { $class = substr($pattern, $i + 1, $end - $i - 1); if ($class[0] === '!') $class = '^'.substr($class, 1); $re .= '['.str_replace(['\\', '~'], ['\\\\', '\~'], $class).']'; $i = $end; } else { $re .= preg_quote($ch, '~'); } } return '~^'.($anchored ? '' : '(?:.*/)?').$re.'$~'; } //////// 保存 private static function load(string $file): array { $c = json_decode((string)@file_get_contents($file), true); if (!is_array($c) || ($c['version'] ?? 0) !== self::VERSION || !is_array($c['trees'] ?? null)) return ['version' => self::VERSION, 'head' => '', 'trees' => []]; $c['head'] = (string)($c['head'] ?? ''); return $c; } private static function save(Repository $repo, string $file, array $c): void { try { $repo->writeFile($file, (string)json_encode($c, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } catch (GitException) { // 保存出来なくても表示は出来る } } }