Class: Canopus::Project
- Inherits:
-
Object
- Object
- Canopus::Project
- Defined in:
- lib/canopus/project.rb,
sig/services.rbs
Defined Under Namespace
Modules: _NativeWatcher Classes: Tree, Watcher
Instance Attribute Summary collapse
-
#root ⇒ String
readonly
Returns the value of attribute root.
Instance Method Summary collapse
-
#files(include_hidden: true, follow_symlinks: false, include_symlinks: false, include_directories: false, extensions: nil, max_size: nil) {|arg0| ... } ⇒ Object
Paths are relative to root, sorted, and never include .git directories.
- #ignore_matcher ⇒ Thuban::IgnoreMatcher
- #ignored?(relative, directory: File.directory?(path(relative))) ⇒ Boolean
-
#initialize(root) ⇒ Project
constructor
A new instance of Project.
- #path(relative) ⇒ String
-
#replace(pattern, replacement, workers: 4, max_size: 10 * 1024 * 1024, extensions: nil, limit: nil, case_sensitive: true, cancelled: nil, paths: nil) ⇒ Hash[String, Integer]
Each file is replaced atomically; a concurrent write aborts that file.
- #watcher(**options) ⇒ Watcher
Constructor Details
Instance Attribute Details
#root ⇒ String (readonly)
Returns the value of attribute root.
14 15 16 |
# File 'lib/canopus/project.rb', line 14 def root @root end |
Instance Method Details
#files(include_hidden:, follow_symlinks:, include_symlinks:, include_directories:, extensions:, max_size:) ⇒ Enumerator[String, self] #files(include_hidden:, follow_symlinks:, include_symlinks:, include_directories:, extensions:, max_size:) ⇒ self
Paths are relative to root, sorted, and never include .git directories. Directory symlinks are opt-in and visited at most once per inode.
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 |
# File 'lib/canopus/project.rb', line 29 def files(include_hidden: true, follow_symlinks: false, include_symlinks: false, include_directories: false, extensions: nil, max_size: nil) return enum_for(__method__, include_hidden: include_hidden, follow_symlinks: follow_symlinks, include_symlinks: include_symlinks, include_directories: include_directories, extensions: extensions, max_size: max_size) unless block_given? extensions = extensions&.map { |extension| extension.start_with?(".") ? extension : ".#{extension}" }&.to_set visited = Set.new base_rules = Thuban::IgnoreMatcher.new exclude = File.join(root, ".git", "info", "exclude") base_rules = base_rules.add(File.read(exclude), base: "") if File.file?(exclude) walk = lambda do |directory, rules| stat = File.stat(path(directory)) return unless visited.add?([stat.dev, stat.ino]) %w[.gitignore .ignore].each do |name| source = path(directory.empty? ? name : File.join(directory, name)) rules = rules.add(File.read(source, encoding: "UTF-8"), base: directory) if File.file?(source) end Dir.children(path(directory)).sort.each do |name| next if name == ".git" || (!include_hidden && name.start_with?(".")) relative = directory.empty? ? name : File.join(directory, name) next if relative == ".canopus/trash" absolute = path(relative) begin entry = File.lstat(absolute) symbolic = entry.symlink? if symbolic && !follow_symlinks next unless include_symlinks next if rules.ignored?(relative) next if extensions && !extensions.include?(File.extname(name)) yield relative next end entry = File.stat(absolute) if symbolic next if rules.ignored?(relative, directory: entry.directory?) if entry.directory? yield relative + "/" if include_directories walk.call(relative, rules) elsif entry.file? next if max_size && entry.size > max_size next if extensions && !extensions.include?(File.extname(name)) yield relative end rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP next # An editor walk tolerates concurrent deletion and inaccessible files. end end end walk.call("", base_rules) self end |
#ignore_matcher ⇒ Thuban::IgnoreMatcher
96 97 98 |
# File 'lib/canopus/project.rb', line 96 def ignore_matcher Thuban::IgnoreMatcher.load(root, global: false).add("/.canopus/trash/\n") end |
#ignored?(relative, directory: File.directory?(path(relative))) ⇒ Boolean
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/canopus/project.rb', line 78 def ignored?(relative, directory: File.directory?(path(relative))) pieces = relative.sub(%r{\A\./}, "").split("/") rules = Thuban::IgnoreMatcher.new base = "" exclude = File.join(root, ".git", "info", "exclude") rules = rules.add(File.read(exclude), base: "") if File.file?(exclude) pieces.each_with_index do |piece, index| return true if piece == ".git" %w[.gitignore .ignore].each do |name| source = path(base.empty? ? name : File.join(base, name)) rules = rules.add(File.read(source), base: base) if File.file?(source) end base = base.empty? ? piece : "#{base}/#{piece}" return true if rules.ignored?(base, directory: index < pieces.length - 1 || directory) end false end |
#path(relative) ⇒ String
21 22 23 24 25 |
# File 'lib/canopus/project.rb', line 21 def path(relative) absolute = File.(relative, root) raise ArgumentError, "path outside project" unless absolute == root || absolute.start_with?(root + File::SEPARATOR) absolute end |
#replace(pattern, replacement, workers: 4, max_size: 10 * 1024 * 1024, extensions: nil, limit: nil, case_sensitive: true, cancelled: nil, paths: nil) ⇒ Hash[String, Integer]
Each file is replaced atomically; a concurrent write aborts that file.
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 |
# File 'lib/canopus/project.rb', line 103 def replace(pattern, replacement, workers: 4, max_size: 10 * 1024 * 1024, extensions: nil, limit: nil, case_sensitive: true, cancelled: nil, paths: nil) expression = pattern.is_a?(Regexp) ? pattern : Regexp.new(Regexp.escape(pattern), case_sensitive ? 0 : Regexp::IGNORECASE) matches = Alkaid::Search.new(root, pattern: expression, ignore: ignore_matcher, workers: workers, max_file_size: max_size, max_matches: limit, extensions: extensions, paths: paths, hidden: true, cancelled: cancelled).run paths = matches.map(&:path).uniq paths.to_h do |relative| absolute = path(relative) before = File.stat(absolute) text = File.read(absolute, encoding: "UTF-8") count, updated = Canopus.with_regexp_timeout(expression) do [text.scan(expression).length, text.gsub(expression, replacement)] end Tempfile.create([".canopus-", ".tmp"], File.dirname(absolute)) do |temp| temp.binmode temp.chmod(before.mode & 0o777) temp.write(updated) temp.flush temp.fsync temp.close current = File.stat(absolute) unless [before.ino, before.size, before.mtime, before.ctime] == [current.ino, current.size, current.mtime, current.ctime] raise IOError, "file changed during replacement: #{relative}" end File.rename(temp.path, absolute) end [relative, count] end end |