~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A copy of a repository shows its commits' authors as the original does

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/commit-authors
crates/knotless-serve/src/index.rs+86−0
@@ -653,6 +653,14 @@ pub(crate) fn connect(path: &Path) -> rusqlite::Result<(Connection, bool)> {
CREATE TABLE IF NOT EXISTS home (
did TEXT PRIMARY KEY, moved INTEGER NOT NULL, own INTEGER NOT NULL,
checked INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS commit_author (
sha TEXT NOT NULL, address TEXT NOT NULL, repo_did TEXT NOT NULL,
repo_key TEXT NOT NULL, did TEXT NOT NULL,
PRIMARY KEY (sha, address, repo_did, repo_key)) WITHOUT ROWID;
CREATE INDEX IF NOT EXISTS commit_author_repo ON commit_author (repo_did, repo_key);
CREATE TABLE IF NOT EXISTS authors_walked (
repo_did TEXT NOT NULL, repo_key TEXT NOT NULL, tip TEXT NOT NULL,
people TEXT NOT NULL, PRIMARY KEY (repo_did, repo_key));
CREATE TABLE IF NOT EXISTS report (
id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS invite (
@@ -3626,6 +3634,76 @@ impl Index {
rows.collect()
}
/// How far the authors `did`'s repository `key` vouches for were
/// walked: the tip its default branch was at, and who it believed
/// then, as [`Index::vouch_authors`] kept them.
pub fn authors_walked(
&self,
did: &str,
key: &str,
) -> rusqlite::Result<Option<(String, String)>> {
let db = self.readers.take(&self.db);
let mut stmt = db.prepare_cached(
"SELECT tip, people FROM authors_walked WHERE repo_did = ?1 AND repo_key = ?2",
)?;
stmt.query_row([did, key], |row| Ok((row.get(0)?, row.get(1)?)))
.optional()
}
/// Keeps that `did`'s repository `key` vouches for `rows`, each a
/// commit, an address of it ([`address`]) and the account it is,
/// walked to `tip` believing `people`. `afresh` forgets what it
/// vouched for before, as who it believes has changed.
pub fn vouch_authors(
&self,
(did, key): (&str, &str),
(tip, people): (&str, &str),
rows: &[(String, String, String)],
afresh: bool,
) -> rusqlite::Result<()> {
let mut db = self.db.lock().unwrap();
let tx = db.transaction()?;
if afresh {
tx.execute(
"DELETE FROM commit_author WHERE repo_did = ?1 AND repo_key = ?2",
[did, key],
)?;
}
{
let mut put = tx.prepare_cached(
"INSERT OR IGNORE INTO commit_author VALUES (?1, ?2, ?3, ?4, ?5)",
)?;
for (sha, address, by) in rows {
put.execute([sha.as_str(), address, did, key, by])?;
}
}
tx.execute(
"INSERT OR REPLACE INTO authors_walked VALUES (?1, ?2, ?3, ?4)",
[did, key, tip, people],
)?;
tx.commit()
}
/// The accounts the repositories the index knows, other than `did`'s
/// `key`, vouch for as `sha`'s at `address`.
pub fn authors_elsewhere(
&self,
sha: &str,
address: &str,
(did, key): (&str, &str),
) -> rusqlite::Result<Vec<String>> {
let db = self.readers.take(&self.db);
let mut stmt = db.prepare_cached(
"SELECT DISTINCT c.did FROM commit_author c
JOIN repo r ON r.did = c.repo_did AND r.key = c.repo_key
WHERE c.sha = ?1 AND c.address = ?2
AND NOT (c.repo_did = ?3 AND c.repo_key = ?4)
AND r.uri NOT IN down AND c.did NOT IN down",
)?;
let found = stmt.query_map([sha, address, did, key], |row| row.get(0))?;
found.collect()
}
/// The record of `did`'s repository `key`.
pub fn repo_of(&self, did: &str, key: &str) -> rusqlite::Result<Option<Value>> {
let db = self.readers.take(&self.db);
@@ -4351,6 +4429,14 @@ fn bot_on(author: &str, did: &str, key: &str) -> String {
)
}
/// How [`Index::vouch_authors`] keeps a commit's address: hashed, as
/// git has it trimmed and lowercased, so the index names no address.
pub fn address(email: &str) -> String {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(email.trim().to_lowercase());
digest.iter().map(|b| format!("{b:02x}")).collect()
}
/// Where a page cut after the record `uri`, made at `at`, starts the next:
/// newest first, ties broken by URI, so the next starts strictly after it.
pub fn cursor_at(at: &str, uri: &str) -> String {
crates/knotless-serve/src/plans.rs+2−1
@@ -32,7 +32,7 @@ pub const ENV: &str = "KNOTLESS_PLANS";
/// The tables that grow with what people do, as many rows as prod might
/// hold: a scan of one is a scan of the network. The rest are said to be
/// small, as a scan of them is.
pub const GROWING: [&str; 19] = [
pub const GROWING: [&str; 20] = [
"forge",
"check_run",
"comment",
@@ -52,6 +52,7 @@ pub const GROWING: [&str; 19] = [
"shared",
"shared_use",
"record_rev",
"commit_author",
];
/// The rows a growing table is said to hold, and a small one.
crates/knotless-serve/src/web.rs+111−10
@@ -1077,20 +1077,31 @@ struct Vouched {
only: Option<HashSet<String>>,
}
/// `name` at `email`, by account if `email` is one of `people`'s in `sha`.
/// Which of `people` `email` is in `sha`, if any.
fn vouched_as<'a>(people: &'a [Vouched], sha: &str, email: &str) -> Option<&'a str> {
let by = people.iter().find(|p| {
let here = p.only.as_ref().is_none_or(|only| only.contains(sha));
here && knotless::emails::is(&p.did, email, &p.hashes)
});
by.map(|p| p.did.as_str())
}
/// `name` at `email`, by account if `email` is one of `people`'s in `sha`,
/// or else the one account other repositories with `sha` vouch for as it
/// there ([`walk_authors`]): none when they disagree.
async fn author(
app: &App,
people: &[Vouched],
(repo, people): (&Repo, &[Vouched]),
sha: &str,
name: String,
email: &str,
) -> CommitAuthorView {
let by = people.iter().find(|p| {
let here = p.only.as_ref().is_none_or(|only| only.contains(sha));
here && knotless::emails::is(&p.did, email, &p.hashes)
});
let by = match vouched_as(people, sha, email) {
Some(did) => Some(did.to_string()),
None => elsewhere(app, repo, sha, email),
};
let actor = match by {
Some(p) => Some(actor(app, &p.did).await),
Some(did) => Some(actor(app, &did).await),
None => None,
};
let agent = email
@@ -1218,6 +1229,93 @@ async fn vouched(
(people, pulls)
}
/// The one account other repositories with `sha` vouch for as `email`
/// in it, if they agree on one.
fn elsewhere(app: &App, repo: &Repo, sha: &str, email: &str) -> Option<String> {
let index = app.index.as_ref()?;
let address = crate::index::address(email);
let found = index.authors_elsewhere(sha, &address, (&repo.did, &repo.key));
let mut found = found.ok()?;
found.pop().filter(|_| found.is_empty())
}
/// The repositories whose authors are being walked now.
static WALKING: LazyLock<Mutex<HashSet<(String, String)>>> = LazyLock::new(Mutex::default);
/// Keeps in the index the account `people` vouch for as each address of
/// each commit on `repo`'s default branch, so that a page of another
/// repository with those commits, as a copy of it, shows them as theirs.
/// Each walk goes on from the tip the last stopped at, and starts over
/// when who `repo` believes changes, as one it no longer believes vouches
/// for nothing. A private repository vouches for nothing elsewhere.
fn walk_authors(app: &App, repo: &Repo, people: Arc<Vec<Vouched>>) {
let index = app.index.clone().filter(|_| repo.in_space.is_none());
let id = (repo.did.clone(), repo.key.clone());
// One walk of a repository at a time.
let index = index.filter(|_| WALKING.lock().unwrap().insert(id.clone()));
if let Some(index) = index {
let (dir, head) = (repo.git.dir.clone(), repo.head.clone());
tokio::task::spawn_blocking(move || {
// One with no tip yet, as an empty repository, has none to walk.
let _ = vouch_walked(&index, &id, &dir, &head, &people);
WALKING.lock().unwrap().remove(&id);
});
}
}
/// [`walk_authors`]'s walk.
fn vouch_walked(
index: &Index,
(did, key): &(String, String),
dir: &std::path::Path,
head: &str,
people: &[Vouched],
) -> anyhow::Result<()> {
let believed = believed(people);
let objects = objects::open(dir)?;
let tip = objects.resolve(head)?;
let walked = index.authors_walked(did, key)?;
let from = match &walked {
Some((at, was)) if *at == tip && *was == believed => return Ok(()),
Some((at, was)) if *was == believed => Some(at.as_str()),
_ => None,
};
let known: Vec<&str> = from.into_iter().collect();
let mut rows = Vec::new();
for c in objects.brought(&tip, &known, usize::MAX)? {
let helped = coauthors(&c.message).into_iter().map(|(_, email)| email);
let emails = [c.author.email, c.committer.email]
.into_iter()
.chain(helped);
for email in emails {
if let Some(by) = vouched_as(people, &c.sha, &email) {
rows.push((c.sha.clone(), crate::index::address(&email), by.to_string()));
}
}
}
index.vouch_authors((did, key), (&tip, &believed), &rows, from.is_none())?;
Ok(())
}
/// Who `people` are, as a walk of the authors they vouch for keeps it:
/// each account and the addresses it claims, but not the pulls merged,
/// as one merged since brought in only commits a walk on from its tip
/// finds.
fn believed(people: &[Vouched]) -> String {
let mut each: Vec<String> = people
.iter()
.map(|p| {
let mut hashes = p.hashes.clone();
hashes.sort();
let everywhere = if p.only.is_none() { "*" } else { "" };
format!("{}{everywhere}:{}", p.did, hashes.join(","))
})
.collect();
each.sort();
each.dedup();
crate::index::address(&each.join("\n"))
}
/// `commits`, as the pages show them, each with the accounts that wrote it
/// where `repo`'s pages believe their claims (`vouched`).
async fn shown(app: &App, repo: &Repo, commits: Vec<read::Commit>) -> Vec<CommitView> {
@@ -1225,6 +1323,9 @@ async fn shown(app: &App, repo: &Repo, commits: Vec<read::Commit>) -> Vec<Commit
let made = assembled(app, repo).await.unwrap_or_default();
let items = &made.items;
let (people, pulls) = vouched(app, repo, record, items).await;
let people = Arc::new(people);
walk_authors(app, repo, people.clone());
let people = (repo, people.as_slice());
let mut out = Vec::new();
for c in commits {
let (pull, message) = match from_pull(items, &c.message) {
@@ -1233,11 +1334,11 @@ async fn shown(app: &App, repo: &Repo, commits: Vec<read::Commit>) -> Vec<Commit
};
let mut helped = Vec::new();
for (name, email) in coauthors(&message) {
helped.push(author(app, &people, &c.sha, name, &email).await);
helped.push(author(app, people, &c.sha, name, &email).await);
}
let (a, m) = (&c.author, &c.committer);
let by = author(app, &people, &c.sha, a.name.clone(), &a.email).await;
let made = author(app, &people, &c.sha, m.name.clone(), &m.email).await;
let by = author(app, people, &c.sha, a.name.clone(), &a.email).await;
let made = author(app, people, &c.sha, m.name.clone(), &m.email).await;
let same = match (&by.actor, &made.actor) {
(Some(x), Some(y)) => x.did == y.did,
_ => (&a.name, &a.email) == (&m.name, &m.email),
crates/knotless-serve/tests/web.rs+51−0
@@ -964,6 +964,57 @@ fn blaming_a_file() {
}
}
#[test]
fn a_copy_shows_whom_the_original_vouched_for() {
let site = serve(true);
let owner = person(account(), &site);
let url = owner.url("original");
ok(&owner, &["create", &url]);
let work = owner.work("w", 2);
// The first push claims git's address as the owner's.
owner.git(&work, &["push", "-q", &url, "main"]);
let first = owner.git(&work, &["rev-parse", "HEAD^"]);
let o = Value::from(owner.account.handle.as_str());
let (copier, taker) = (person(account(), &site), person(account(), &site));
let author = |who: &Sandbox, name: &str, sha: &str| {
let at = format!("/{}/{name}/commit/{sha}", who.account.handle);
wait_until(|| site.status(&self::at(&at)).0 == 200);
site.data(&at)["commit"]["author"]["actor"]["handle"].clone()
};
// A copy, pushed as a repository of its own rather than forked, by
// one who claims another address, shows the owner's commits as no
// one's until the original's pages have walked them, then as the
// owner's: the original's owner vouched for them there.
ok(&copier, &["email", "add", "copier@example.com"]);
let copy = copier.url("copy");
ok(&copier, &["create", &copy]);
copier.git(&work, &["push", "-q", &copy, "main"]);
assert_eq!(author(&copier, "copy", &first), Value::Null);
assert_eq!(author(&owner, "original", &first), o);
wait_until(|| author(&copier, "copy", &first) == o);
// Shown again with nothing new, the walk has nothing to do; a commit
// pushed since is walked on from where the last stopped.
assert_eq!(author(&owner, "original", &first), o);
let tip = owner.commit_file(&work, "g", "g\n");
owner.git(&work, &["push", "-q", &url, "main"]);
copier.git(&work, &["push", "-q", &copy, "main"]);
assert_eq!(author(&owner, "original", &tip), o);
wait_until(|| author(&copier, "copy", &tip) == o);
// One who copies them and claims the owner's address, as their own
// first push does, has them shown as theirs only in their own
// repository. Elsewhere the two disagree, and the copy shows the
// commits as no one's rather than either's.
let taken = taker.url("taken");
ok(&taker, &["create", &taken]);
taker.git(&work, &["push", "-q", &taken, "main"]);
let t = Value::from(taker.account.handle.as_str());
wait_until(|| author(&taker, "taken", &first) == t);
wait_until(|| author(&copier, "copy", &first) == Value::Null);
assert_eq!(author(&owner, "original", &first), o);
}
#[test]
fn comments_on_lines() {
let site = serve(true);
docs/design/identity.md+25−0
@@ -154,6 +154,31 @@ The owner and collaborators are matched first, so a pull's author cannot
take their addresses. Someone who claims your address gains nothing beyond
the commits they got merged.
### Commits in other repositories
A commit keeps its ID when its history is pushed somewhere else, as in a
fork, a mirror, or a copy pushed as a repository of its own. Where a
page's own repository believes no claim for an address in a commit, it
asks which accounts other repositories with that commit believe there:
- Exactly one: the page shows that account.
- More than one: the page shows none, rather than pick between them.
- None: the address stays unlinked.
So a copy shows its commits' authors as the original shows them. Someone
who copies your commits and claims your address is believed only in their
own repository. Everywhere else the claims disagree, and the commits show
as no one's rather than as theirs.
What each repository believes comes from a walk of its default branch,
kept in the index. The walk runs in the background when a page shows the
repository's commits, and goes on from the tip where the last walk
stopped. It starts over when the people the repository believes change,
or when their claims change, since an address it no longer believes
vouches for nothing. Only public repositories the index knows count, and
not those taken down. A private repository vouches for nothing outside
itself.
A commit knotless makes for an account that names no author is authored as
`<did>@knotless.invalid`, which needs no claim.
docs/design/serve.md+3−1
@@ -541,7 +541,9 @@ it log in again.
The index records its table version. A build with another version drops the
cache tables and rebuilds them from scratch, keeping only takedowns, reports,
emitted labels, pushes, repositories' [languages](#languages), which accounts are hidden ([Accounts that
emitted labels, pushes, repositories' [languages](#languages), the commit
authors each repository vouches for ([Commits in other
repositories](identity.md#commits-in-other-repositories)), which accounts are hidden ([Accounts that
go](#accounts-that-go)), and, in the index of private repositories, which
members were proven to read which space. Pushes are kept because nothing can bring them
back: each is read from a claim, compaction deletes claims a day old, and the