module Backlog # Validates and classifies tracker keys (the value written to # `.cc-os/config`'s `tracker` field) and parses `git remote -v` output to # detect a Forgejo/GitHub remote. Pure — no filesystem, no network. class Tracker DEFAULT_FORGEJO_HOST = "forgejo.swansoncloud.com" # ADR-0042 retired Planka: the tracker grammar is exactly these three # forms. `planka:` is deliberately absent — it is rejected, not merely # unmatched (see config-write and os-status's tracker-configured check). FORMATS = { forgejo: %r{\Aforgejo:[\w.-]+/[\w.-]+\z}, github: %r{\Agithub:[\w.-]+/[\w.-]+\z}, repo: /\Arepo:.+\z/ }.freeze # @return [Boolean] true if value matches one of the three valid formats def self.valid?(value) !kind(value).nil? end # @return [Symbol, nil] :forgejo / :github / :repo, or nil if invalid def self.kind(value) return nil unless value.is_a?(String) FORMATS.each { |name, pattern| return name if value.match?(pattern) } nil end # Where /to-issues output (spec slices) should be published, given the # repo's tracker key. One tracker holds both state and specs (ADR-0042): # forgejo:/github: keys route spec slices to that git issue tracker; # repo: routes to the in-repo issue-file convention; nil or any # invalid key (including retired `planka:` values) is :unrouted. # # @return [Symbol] :git_issues | :repo_files | :unrouted def self.issues_destination(value) case kind(value) when :forgejo, :github then :git_issues when :repo then :repo_files else :unrouted end end # Parse the first remote line of `git remote -v` output and classify it # as a Forgejo or GitHub remote (or :unknown for anything else). # # @param output [String] raw `git remote -v` output # @param forgejo_host [String] hostname that identifies the user's # self-hosted Forgejo instance # @return [Hash, nil] {kind:, host:, owner:, repo:}, or nil if no remote # URL could be parsed def self.parse_remote(output, forgejo_host: DEFAULT_FORGEJO_HOST) line = output.to_s.lines.first return nil unless line url = line.split(/\s+/)[1] return nil unless url host, path = split_host_path(url) return nil unless host && path owner, repo = path.sub(/\.git\z/, "").split("/").last(2) return nil unless owner && repo kind = if host == forgejo_host :forgejo elsif host == "github.com" :github else :unknown end { kind: kind, host: host, owner: owner, repo: repo } end def self.split_host_path(url) if url =~ %r{\A[\w+.-]+://(?:[^/@]+@)?([^:/]+)(?::\d+)?/(.+)\z} [Regexp.last_match(1), Regexp.last_match(2)] elsif url =~ /\A[^@\s]+@([^:]+):(.+)\z/ [Regexp.last_match(1), Regexp.last_match(2)] end end private_class_method :split_host_path end end