Class: Agent::Lock::Scope

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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.

Returns:

  • (String) —

    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.

Parameters:

  • text (String) —

    relative to the tree root

  • tree (Tree)

Returns:

  • (Boolean) —

    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.

Parameters:

  • text (String)

Returns:

  • (Boolean) —

    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.

Parameters:

  • path (String) —

    a path or a glob, absolute or relative

  • tree (Tree)

Returns:

Raises:

  • (Invalid) —

    when the scope is empty, or resolves outside the tree



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

Parameters:

Returns:

  • (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.

Examples:

Scope.new("lib/**").covers?(Scope.new("lib/cli.rb"))   # => true
Scope.new("lib/cli.rb").covers?(Scope.new("lib/**"))   # => false

Parameters:

Returns:

  • (Boolean)


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.

Returns:

  • (String) —

    "" for a pattern that starts with a wildcard



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.

Returns:

  • (Boolean) —

    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.

Returns:

  • (String) —

    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