Skip to content
quarry Artifact [92d22e9db3]
Login

Artifact [92d22e9db3]

Artifact 92d22e9db338b8028f49d1a0ccadb4b5fdb25d7c1b275cede0e7618fcf230ca1:


// closing.go reads the two sentences a landing may say about tickets:
// that accepting it finishes them, and that it works on them without
// finishing them.
//
// Both are the same grammar and the same link. The difference is only
// what the accept path does with each: a closes settles the ticket, an
// addresses settles nothing. The second verb exists because most work
// is the second case — a landing that moves a ticket forward without
// ending it had no way to say so, so it either said nothing (and the
// ticket showed no sign the work existed) or said "closes" (and the
// ticket was wrongly shut).
//
// The grammar is deliberately the SAME one the renderer links with —
// ParseRef, not a second pattern — so a reference that closes a ticket
// is a reference a reader can click, and there is no way to write one
// that acts without also appearing. A closing phrase quarry acts on
// must be a phrase quarry shows.
package markup

import (
	"regexp"
	"strings"
)

// closingPhrase is a closing verb and the token it governs.
//
// ADJACENCY is the safety, not a short verb list. The verb must be
// immediately followed by the reference, with nothing but spaces or a
// colon between, which is what separates a statement of intent from
// prose that happens to contain both words: "closes ae12f3" acts, and
// "this closes the gap that ae12f3 describes" does not, because in the
// second the token is four words away. Given that rule, refusing
// "fixed ae12f3" buys no safety and costs a reader the link they
// plainly asked for — a landing message written in the past tense is
// the common case, not the exception.
//
// So: the three tenses of the three verbs, which is GitHub's set and
// the one people already have in their fingers.
var closingPhrase = regexp.MustCompile(
	`(?i)\b(?:close[sd]?|closing|fix(?:e[sd])?|fixing|resolve[sd]?|resolving)\b:?[ \t]+(\S+)`)

// addressingPhrase is the same shape for the phrases that LINK without
// settling.
//
// Wider than the closing set on purpose, and safe to be: a false
// positive here links a ticket a reader can see is linked, where a
// false positive on a close shuts somebody's ticket. So the words
// people actually write when a landing moves a ticket without ending
// it all count, in whatever tense.
//
// "re", "see" and "for" are deliberately absent. Each precedes a
// reference in ordinary prose far too often to read as a declaration —
// "see 2058808c3504 for the numbers", "waited for 3600000" — and the
// adjacency rule cannot help, because there the reference IS the next
// token.
var addressingPhrase = regexp.MustCompile(
	`(?i)\b(?:address(?:e[sd])?|addressing|` +
		`refs?|references?|referenced|referencing|` +
		`part of|relates? to|related to|relating to|` +
		`works? on|working on|toward|towards|` +
		`progress(?:es)? on|contributes? to)\b:?[ \t]+(\S+)`)

// ClosingRefs lists the references a message declares it closes, in
// order and without duplicates.
//
// Parsing only: it says what the text CLAIMS, never what exists. The
// caller resolves each token and decides which are tickets it may act
// on — a landing cannot close something by naming it.
func ClosingRefs(text string) []RefToken { return refsAfter(closingPhrase, text) }

// AddressingRefs lists the references a message declares it addresses:
// work on that ticket that does not finish it.
//
// Parsed by the same rules and refused for the same reasons as a close.
// The caller is expected to LINK these and act on none of them.
func AddressingRefs(text string) []RefToken { return refsAfter(addressingPhrase, text) }

// refsAfter is the shared reading: one verb pattern, the token it
// governs, in order and without duplicates.
func refsAfter(pat *regexp.Regexp, text string) []RefToken {
	var out []RefToken
	seen := map[string]bool{}
	for _, m := range pat.FindAllStringSubmatch(text, -1) {
		// The token as written wins. Only if that is not a reference is
		// trailing punctuation stripped and the shorter form tried —
		// the other order eats the closing bracket of "[ae12f3]",
		// which is the canonical spelling of a bare hash and so the
		// one form that must never fail to parse.
		tok, ok := ParseRef(m[1])
		if !ok {
			tok, ok = ParseRef(strings.TrimRight(m[1], ".,;:!?)]}"))
		}
		if !ok {
			continue
		}
		// A bare "#12" reads as a chat line elsewhere, but a chat line
		// is never a thing to close or address: after one of these
		// verbs it is the ticket numbered 12, as "Closes #12" is on a
		// pull request.
		if tok.Kind == RefChat {
			tok.Kind = RefTicket
		}
		// A person is never a thing to close, nor a landing (!12): a
		// landing settles by its own accept or reject. Both parse as
		// tokens, so they are refused here rather than left for the
		// caller to notice.
		if tok.Kind == RefPerson || tok.Kind == RefLanding {
			continue
		}
		c := tok.Canonical()
		if seen[c] {
			continue
		}
		seen[c] = true
		out = append(out, tok)
	}
	return out
}