Skip to content
quarry Artifact [a291017949]
Login

Artifact [a291017949]

Artifact a29101794986f653d9a2d8d5648413cd320654d846512138edd3156720f85a89:


// teamhands.go — what happens when a repository changes hands.
//
// Putting a repository under a team grants every member whatever the
// membership floor and their team row carry, on that repository. Taking
// it out takes that away. Moving it between teams does both at once, to
// two different populations. None of that was told to anybody: the
// single-account path (account.caps) raises RFC 0005's vocabulary and
// puts a line in the person's inbox, and the TEAM path — which changes
// access for many people in one operation, and on a private repository
// changes the whole of it — was silent.
//
// So this file reuses the two functions account.caps already uses,
// per affected account, rather than inventing a team vocabulary. The
// words a person reads are the same whether their access changed
// because somebody edited their row or because a repository moved,
// which is the honest description in both cases: what changed is what
// they can reach.
package api

import (
	"context"
	"fmt"
	"sort"
	"strings"
)

// capsText renders an effective set as the string the vocabulary and
// the inbox already speak in. CapSet is an interface here (the api
// package does not import oltp's concrete type for this), so the text
// comes from its Stringer the way every other surface reads it.
func capsText(c CapSet) string {
	if c == nil {
		return ""
	}
	return fmt.Sprint(c)
}

// handsWatch is the access picture before a repository changes hands:
// the effective string of everybody who could be affected.
//
// Captured BEFORE the write, because "what changed" needs both sides
// and the store only holds one of them at a time.
type handsWatch struct {
	repo   string
	before map[string]string
}

// watchHands snapshots the accounts whose access to repo could move:
// the members of every team named. Callers pass the team losing it and
// the team gaining it; either may be empty.
//
// Bounded by the teams' membership, not by the forge.
func (d Deps) watchHands(repo string, teams ...string) handsWatch {
	w := handsWatch{repo: repo, before: map[string]string{}}
	if d.CapsFor == nil || d.Teams == nil {
		return w
	}
	for _, team := range teams {
		if team == "" {
			continue
		}
		members, err := d.Teams.TeamMembers(team)
		if err != nil {
			continue
		}
		for _, m := range members {
			if _, seen := w.before[m.Account]; seen {
				continue
			}
			w.before[m.Account] = capsText(d.CapsFor(m.Account, repo))
		}
	}
	return w
}

// settled tells everybody whose access actually moved, and raises the
// vocabulary event for each.
//
// Called AFTER the write and never allowed to fail it: the repository
// has already changed hands, so an error here would report a change
// that did happen as one that did not. Accounts whose effective string
// is unchanged are silent — a member of both teams loses nothing when a
// repository moves between them, and must not be told they did.
func (d Deps) settled(ctx context.Context, w handsWatch, actor string) {
	if d.CapsFor == nil || w.repo == "" {
		return
	}
	names := make([]string, 0, len(w.before))
	for account := range w.before {
		names = append(names, account)
	}
	sort.Strings(names)
	for _, account := range names {
		before := w.before[account]
		after := capsText(d.CapsFor(account, w.repo))
		if before == after {
			continue
		}
		d.raiseAccountChange(ctx, w.repo, account, before, after)
		d.AccessChanged(ctx, actor, account, w.repo, before, after)
	}
}

// wouldLose is who loses access to repo if it leaves team: the members
// whose only route to it is the membership floor. Somebody holding a
// string of their own on that repository keeps it and is not named.
//
// Shared by handsWarning (which says it after) and team.get (which says
// it before), so the sentence on the control and the sentence in the
// answer cannot describe one change two ways.
func (d Deps) wouldLose(team, repo string) []string {
	if team == "" || d.Teams == nil {
		return nil
	}
	members, err := d.Teams.TeamMembers(team)
	if err != nil {
		return nil
	}
	// On a public repository the categories already grant what the
	// floor does, so nobody's access actually moves. Asked once for the
	// repository rather than once per member.
	if d.CapsFor != nil && d.CapsFor("", repo) != nil &&
		d.CapsFor("", repo).HasAny(teamFloorLetters) {
		return nil
	}
	var losing []string
	for _, m := range members {
		if d.Roster != nil {
			if own, cerr := d.Roster.CapsOf(m.Account, repo); cerr == nil && own != "" {
				continue
			}
		}
		losing = append(losing, m.Account)
	}
	sort.Strings(losing)
	return losing
}

// teamFloorLetters is what the membership floor carries that a reader
// would notice losing. Not the whole floor: a public repository grants
// most of it through its categories anyway, and the question here is
// whether leaving the team takes anything away at all.
const teamFloorLetters = "C"

// handsWarning names who loses a repository when it moves out of a
// team, for the op to return before anybody has to discover it.
//
// RFC 0017 set this rule for unbinding a companion — "MUST name who
// loses access before it happens" — on the reasoning that dropping
// access is the safe direction but not a quiet operation. A repository
// leaving a team is the same act on the same population, so it gets the
// same treatment. The forge knows exactly who: the team's members whose
// access to that repository came only through it.
func (d Deps) handsWarning(from, repo string) string {
	losing := d.wouldLose(from, repo)
	if len(losing) == 0 {
		return ""
	}
	who := strings.Join(losing, ", ")
	if len(losing) > 6 {
		who = fmt.Sprintf("%s and %d more", strings.Join(losing[:6], ", "), len(losing)-6)
	}
	return fmt.Sprintf("%s left %s: %s %s access that came only through it",
		repo, from, who, plural(len(losing), "loses", "lose"))
}

// plural picks the verb for a count, because "1 accounts lose" is the
// kind of thing a reader stops trusting the rest of the sentence over.
func plural(n int, one, many string) string {
	if n == 1 {
		return one
	}
	return many
}