Class: Agent::Lock::Scope
- Inherits:
-
Object
- Object
- Agent::Lock::Scope
- Defined in:
- lib/agent/lock/scope.rb
Overview
What a lock covers: one path, or a glob over many.
** is the whole tree, workflow/** a corner of it, lib/a.rb a single
file. A directory is taken to mean everything under it, since an agent
that says it is working in docs is not promising to leave docs/api
alone.
Two scopes conflict when either one's fixed part contains the other's.
workflow/** and workflow/lib/cli.rb conflict; docs/** and
workflow/** do not. Comparing the fixed parts rather than trying to
intersect two globs is deliberate: glob intersection has answers nobody
can predict, and the failure it would buy is two agents editing one file.
This errs the other way, toward refusing work that might have been safe.
Defined Under Namespace
Classes: Invalid
Constant Summary collapse
- ALL =
"**"- WHOLE_TREE =
The spellings of the whole tree, taken literally wherever the agent stands.
[".", "*", ALL].freeze
- EMPTY =
"empty scope: pass ** to claim the whole tree"
Instance Attribute Summary collapse
-
#pattern ⇒ String
readonly
The pattern, relative to the tree root.
Class Method Summary collapse
-
.directory?(text, tree) ⇒ Boolean
A plain path naming a directory that exists.
-
.glob?(text) ⇒ Boolean
Whether it has a wildcard anywhere in it.
-
.parse(path, tree:) ⇒ Scope
What the user typed, as a pattern relative to the tree root.
Instance Method Summary collapse
- #==(other) ⇒ Object
- #conflicts_with?(other) ⇒ Boolean
-
#covers?(other) ⇒ Boolean
Whether holding this scope already means holding
other, which is a stricter question than whether the two overlap. -
#fixed_part ⇒ String
The leading segments with no wildcard in them, which is the deepest directory a pattern is certainly confined to.
-
#initialize(pattern) ⇒ Scope
constructor
A new instance of Scope.
-
#recursive? ⇒ Boolean
The whole tree, or everything under one directory.
-
#slug ⇒ String
Safe to use in a filename.
- #to_s ⇒ Object
Constructor Details
#initialize(pattern) ⇒ Scope
Returns a new instance of Scope.
111 112 113 |
# File 'lib/agent/lock/scope.rb', line 111 def initialize(pattern) @pattern = pattern.to_s.squeeze("/").delete_prefix("./") end |
Instance Attribute Details
#pattern ⇒ String (readonly)
Returns the pattern, relative to the tree root.
36 37 38 |
# File 'lib/agent/lock/scope.rb', line 36 def pattern @pattern end |
Class Method Details
.directory?(text, tree) ⇒ Boolean
Returns a plain path naming a directory that exists.
70 71 72 73 74 |
# File 'lib/agent/lock/scope.rb', line 70 def directory?(text, tree) return false if glob?(text) File.directory?(File.join(tree.root, text)) end |
.glob?(text) ⇒ Boolean
Returns whether it has a wildcard anywhere in it.
65 |
# File 'lib/agent/lock/scope.rb', line 65 def glob?(text) = text.match?(/[*?\[{]/) |
.parse(path, tree:) ⇒ Scope
What the user typed, as a pattern relative to the tree root.
An empty scope used to mean the whole tree, so alock acquire "$SCOPE"
with the variable unset claimed everything, and nobody asked for
that. Globs used to skip the tree entirely, so ../x/** was stored
verbatim and /abs/tree/lib/** never met lib/**. Both are read
the way a plain path is now.
51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/agent/lock/scope.rb', line 51 def parse(path, tree:) text = path.to_s.strip raise Invalid, EMPTY if text.empty? return new(ALL) if WHOLE_TREE.include?(text) text = glob?(text) ? relative_glob(text, tree) : tree.relative(text) return new(ALL) if text == "." text = "#{text}/#{ALL}" if directory?(text, tree) new(text) end |
Instance Method Details
#==(other) ⇒ Object
161 |
# File 'lib/agent/lock/scope.rb', line 161 def ==(other) = other.is_a?(Scope) && pattern == other.pattern |
#conflicts_with?(other) ⇒ Boolean
125 126 127 128 129 |
# File 'lib/agent/lock/scope.rb', line 125 def conflicts_with?(other) return true if pattern == other.pattern contains?(fixed_part, other.fixed_part) || contains?(other.fixed_part, fixed_part) end |
#covers?(other) ⇒ Boolean
Whether holding this scope already means holding other, which is a
stricter question than whether the two overlap. Only a scope that
takes everything under a directory can promise that; a partial glob
such as lib/*.rb covers nothing but itself, since working out what
else it matches is the guessing #conflicts_with? refuses to do.
143 144 145 146 147 |
# File 'lib/agent/lock/scope.rb', line 143 def covers?(other) return true if pattern == other.pattern recursive? && contains?(fixed_part, other.fixed_part) end |
#fixed_part ⇒ String
The leading segments with no wildcard in them, which is the deepest directory a pattern is certainly confined to.
119 120 121 |
# File 'lib/agent/lock/scope.rb', line 119 def fixed_part @fixed_part ||= pattern.split("/").take_while { |part| !self.class.glob?(part) }.join("/") end |
#recursive? ⇒ Boolean
Returns the whole tree, or everything under one directory.
150 |
# File 'lib/agent/lock/scope.rb', line 150 def recursive? = [ALL, "#{fixed_part}/#{ALL}"].include?(pattern) |
#slug ⇒ String
Returns safe to use in a filename.
153 154 155 156 157 |
# File 'lib/agent/lock/scope.rb', line 153 def slug text = pattern.gsub("**", "all").gsub(%r{[^A-Za-z0-9._/-]}, "").tr("/", "-").squeeze("-") text = text.delete_prefix("-").delete_suffix("-") text.empty? ? "tree" : text[0, 60] end |
#to_s ⇒ Object
159 |
# File 'lib/agent/lock/scope.rb', line 159 def to_s = pattern |