Class: Copse::Worktree

Inherits:
Object
  • Object
show all
Defined in:
lib/copse/worktree.rb

Overview

Answers "what am I called, and what port do I get" by shelling out to git.

Holds no state and touches no files. Nothing here needs git to succeed -- git only answers the naming question more precisely. A plain directory, or a machine with no git at all, degrades to the directory name.

Constant Summary collapse

LABEL_LIMIT =
63

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root = Dir.pwd) ⇒ Worktree

Returns a new instance of Worktree.



16
17
18
# File 'lib/copse/worktree.rb', line 16

def initialize(root = Dir.pwd)
  @root = File.expand_path(root)
end

Instance Attribute Details

#rootObject (readonly)

Returns the value of attribute root.



14
15
16
# File 'lib/copse/worktree.rb', line 14

def root
  @root
end

Class Method Details

.slug(value) ⇒ Object

Reduces an arbitrary string to a single valid DNS label.

A whitelist, not a substitution list. Git accepts branch names containing ;, $(, &&, quotes, and more, and the slug flows into COPSE_HOST, COPSE_URL, every child process's environment, and a generated Procfile that a shell will execute -- so anything outside [a-z0-9-] is replaced rather than passed through. Branch names also arrive from collaborators via git fetch; they are not necessarily the developer's own input.

Returns nil when nothing survives, so callers can fall back.



88
89
90
91
92
# File 'lib/copse/worktree.rb', line 88

def self.slug(value)
  collapsed = value.to_s.downcase.gsub(/[^a-z0-9]+/, "-")
  trimmed = collapsed[0, LABEL_LIMIT].to_s.gsub(/\A-+|-+\z/, "")
  trimmed.empty? ? nil : trimmed
end

Instance Method Details

#branchObject



70
71
72
73
74
75
76
# File 'lib/copse/worktree.rb', line 70

def branch
  return @branch if defined?(@branch)

  name = git("rev-parse", "--abbrev-ref", "HEAD")
  # "HEAD" means detached; there is no branch to name the worktree after.
  @branch = (name == "HEAD" ? nil : name)
end

#companion_portObject



30
31
32
# File 'lib/copse/worktree.rb', line 30

def companion_port
  @companion_port ||= Copse.companion_port_for(host)
end

#database_suffixObject

What a linked worktree's development database name is suffixed with, or nil in a main worktree -- which keeps the app's plain database.

The same slug the hostname uses, with - swapped for _: a hyphen is legal in a database name but has to be quoted on every psql and mysql command line, and _ is what Rails' own <app>_development already uses.



44
45
46
# File 'lib/copse/worktree.rb', line 44

def database_suffix
  slug&.tr("-", "_")
end

#hostObject

<project>.localhost for a main worktree, <branch>.<project>.localhost for a linked one.



22
23
24
# File 'lib/copse/worktree.rb', line 22

def host
  @host ||= [slug, project, "localhost"].compact.join(".")
end

#linked?Boolean

Returns:

  • (Boolean)


64
65
66
67
68
# File 'lib/copse/worktree.rb', line 64

def linked?
  return @linked if defined?(@linked)

  @linked = !main_root.nil? && !toplevel.nil? && main_root != toplevel
end

#portObject



26
27
28
# File 'lib/copse/worktree.rb', line 26

def port
  @port ||= Copse.port_for(host)
end

#projectObject

The project name: the main worktree's directory name, even when called from a linked worktree.



50
51
52
# File 'lib/copse/worktree.rb', line 50

def project
  @project ||= self.class.slug(File.basename(main_root)) || "app"
end

#slugObject

nil for a main worktree; the branch name (or this directory's name on a detached HEAD) for a linked one.



56
57
58
59
60
61
62
# File 'lib/copse/worktree.rb', line 56

def slug
  return @slug if defined?(@slug)

  @slug = if linked?
            self.class.slug(branch) || self.class.slug(File.basename(toplevel))
          end
end

#urlObject



34
35
36
# File 'lib/copse/worktree.rb', line 34

def url
  "http://#{host}:#{port}"
end