~ 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

knotless commits as git does on your machine, and a runner as knotless

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:identity/commits-as-git
.changeset/commits-as-git.mdnew+7−0
@@ -0,0 +1,7 @@
---
default: minor
---
# knotless commits as git does on your machine
`knotless repo commit` with no `--author`, `knotless pull merge`, `pull update-branch` and `pull revert`, and the same tools over MCP on stdio, now author and commit as git would there (`user.name` and `user.email`), rather than as `<your did>@knotless.invalid`, which only knotless can tie to you. A history made with knotless reads on GitHub or any mirror as one made with git. With no git identity set, they commit as your account, as before. A runner landing the merge queue or writing back a workflow's files commits as `knotless`, whichever account it runs as.
crates/knotless/src/branch_update.rs+1−1
@@ -110,7 +110,7 @@ pub async fn update_branch(ctx: &Ctx, target: &RepoUrl, pull: &str) -> Result<Up
"\n\nBoth sides only added lines at the same place in {paths}: {into}'s come first."
));
}
let git = forge::committer(git, &me);
let git = forge::committer(ctx, git, &me).await;
let args = ["commit-tree", &tree, "-p", head, "-p", base, "-m", &message];
let commit = git.text(&args, None).await?;
let update = Update {
crates/knotless/src/cli.rs+4−2
@@ -505,11 +505,13 @@ URLs look like knotless://<handle or did>/<name>. Pulls and issues on public
repositories come from an appview: https://knotless.dev, or the one
KNOTLESS_APPVIEW names (set empty, none).";
/// `knotless ...`.
/// `knotless ...`, committing as git does here.
pub fn main(args: Vec<String>) -> i32 {
let mut env = Env::from_process();
env.signer = crate::env::Signer::Git;
main_with(
args,
Env::from_process(),
env,
&mut std::io::stdin().lock(),
&mut std::io::stdout(),
)
crates/knotless/src/commit.rs+9−6
@@ -95,11 +95,14 @@ pub fn start(snap: &Snapshot, spec: &Spec) -> Option<String> {
})
}
/// The commit's author: as given, or whoever is committing.
fn signed(spec: &Spec, me: &Session) -> Spec {
/// The commit's author: as given, or whoever is committing, as they commit
/// here ([`crate::emails::signature`]).
async fn signed(ctx: &Ctx, spec: &Spec, me: &Session) -> Spec {
let mut spec = spec.clone();
spec.author
.get_or_insert_with(|| (me.handle.clone(), crate::emails::own(&me.did)));
if spec.author.is_none() {
let by = crate::emails::signature(ctx, "GIT_AUTHOR_IDENT", &me.did, &me.handle);
spec.author = Some(by.await);
}
spec
}
@@ -108,10 +111,10 @@ pub async fn commit(ctx: &Ctx, url: &RepoUrl, spec: &Spec) -> Result<Committed>
if let Some(me) = session::reader(ctx, &remote.identity).await
&& (me.did != remote.did() || spec.via_gateway)
{
return gateway::commit(ctx, &remote, &me, &signed(spec, &me)).await;
return gateway::commit(ctx, &remote, &me, &signed(ctx, spec, &me).await).await;
}
let session = session::ensure(ctx, &remote.identity).await?;
let spec = &signed(spec, &session);
let spec = &signed(ctx, spec, &session).await;
let refname = format!("refs/heads/{}", spec.branch);
let mut attempts = 0;
loop {
crates/knotless/src/emails.rs+38−7
@@ -4,7 +4,7 @@
//! and one address claimed by two accounts hashes apart in each. Pages
//! believe a claim only from a repository's owner and collaborators.
use crate::env::Ctx;
use crate::env::{Ctx, Signer};
use crate::git::Git;
use crate::records::{self, EmailsRecord, nsid};
use crate::remote::{self, Remote};
@@ -33,6 +33,37 @@ pub fn own(did: &str) -> String {
format!("{did}{OWN}")
}
/// Who a commit knotless makes for `did`, known as `handle`, says made it,
/// as [`Signer`] says: `which` is `GIT_AUTHOR_IDENT` for its author,
/// `GIT_COMMITTER_IDENT` for its committer.
pub async fn signature(ctx: &Ctx, which: &str, did: &str, handle: &str) -> (String, String) {
let here = match ctx.env.signer {
Signer::Git => git_ident(which).await,
Signer::Account | Signer::Runner => None,
};
let name = match ctx.env.signer {
Signer::Runner => "knotless",
Signer::Account | Signer::Git => handle,
};
here.unwrap_or_else(|| (name.to_string(), own(did)))
}
/// Whom git commits as here, from its environment or config, its own
/// repository's here included: none where git would guess an address from
/// the machine's name.
async fn git_ident(which: &str) -> Option<(String, String)> {
let args = ["-c", "user.useConfigOnly=true", "var", which];
let said = Git::from_env().text(&args, None).await.ok()?;
ident(&said)
}
/// The name and address of one of git's identities, `Name <email> date`.
fn ident(said: &str) -> Option<(String, String)> {
let (name, rest) = said.split_once('<')?;
let (email, _) = rest.split_once('>')?;
Some((name.trim().to_string(), email.trim().to_string()))
}
/// Whether `email` is `did`'s: the address knotless commits as for it, or
/// one whose hash it claims.
pub fn is(did: &str, email: &str, hashes: &[String]) -> bool {
@@ -107,12 +138,12 @@ pub async fn claim_first(ctx: &Ctx, git: &Git, me: &Session) -> Result<Option<St
return Ok(None);
}
// As git would author a commit here: from its environment or config.
let ident = git.text(&["var", "GIT_AUTHOR_IDENT"], None).await;
let ident = ident.unwrap_or_default();
let email = ident
.split_once('<')
.and_then(|(_, rest)| rest.split_once('>'));
let email = email.map_or("", |(email, _)| email.trim()).to_string();
let said = git.text(&["var", "GIT_AUTHOR_IDENT"], None).await;
let email = said
.ok()
.and_then(|said| ident(&said))
.map(|(_, email)| email);
let email = email.unwrap_or_default();
let first = email.contains('@') && claim(&account(ctx, &me.did).await?).await?.is_none();
if first {
set(ctx, me, &email, true).await?;
crates/knotless/src/env.rs+18−0
@@ -23,6 +23,21 @@ pub enum HandleResolver {
Xrpc { service: String },
}
/// Whom the commits knotless makes for an account say made them, where no
/// author is named; each falls back to [`Signer::Account`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Signer {
/// The account: its handle, at `<did>@knotless.invalid`. A server
/// acting for accounts, which knows no one's git.
Account,
/// Whom git commits as here, as `git commit` would take it: on a
/// person's own machine, the CLI and MCP over stdio.
Git,
/// `knotless`, at the account's `<did>@knotless.invalid`: a runner,
/// whichever account it runs as.
Runner,
}
/// The appview a process reads pulls and issues from when
/// `KNOTLESS_APPVIEW` is unset.
pub const DEFAULT_APPVIEW: &str = "https://knotless.dev";
@@ -91,6 +106,8 @@ pub struct Env {
pub ignore_minimum: bool,
/// Where `knotless upgrade` installs, when not beside this binary.
pub install_dir: Option<PathBuf>,
/// Whom commits made here with no author named say made them.
pub signer: Signer,
}
impl Env {
@@ -171,6 +188,7 @@ impl Env {
install_dir: get("KNOTLESS_INSTALL_DIR")
.filter(|v| !v.is_empty())
.map(PathBuf::from),
signer: Signer::Account,
}
}
}
crates/knotless/src/forge.rs+16−13
@@ -17,6 +17,7 @@
use crate::approvals;
use crate::checks::{self, Standing};
use crate::closing;
use crate::emails;
use crate::env::Ctx;
use crate::fetch;
use crate::fix::{Fix, Refused};
@@ -2468,9 +2469,9 @@ pub async fn merge(
let merged = if there {
head.clone()
} else if squash {
squash_commit(git, &tree, base, head, &item, &me).await?
squash_commit(ctx, git, &tree, base, head, &item, &me).await?
} else {
merge_commit(git, &tree, base, head, &item, &me).await?
merge_commit(ctx, git, &tree, base, head, &item, &me).await?
};
let others = assemble(&t.record, t.remote.did(), &entries);
let spent = (!keep).then(|| spent(t.facts(), &item, &others)).flatten();
@@ -3410,20 +3411,20 @@ fn nobody(git: &Git) -> Git {
git
}
/// `git`, committing as `me`.
pub(crate) fn committer(git: &Git, me: &Session) -> Git {
/// `git`, committing as `me` does here: see [`emails::signature`].
pub(crate) async fn committer(ctx: &Ctx, git: &Git, me: &Session) -> Git {
let mut git = git.clone();
let email = format!("{}@knotless.invalid", me.did);
git.envs.extend([
("GIT_AUTHOR_NAME".into(), me.handle.clone()),
("GIT_AUTHOR_EMAIL".into(), email.clone()),
("GIT_COMMITTER_NAME".into(), me.handle.clone()),
("GIT_COMMITTER_EMAIL".into(), email),
]);
for who in ["AUTHOR", "COMMITTER"] {
let which = format!("GIT_{who}_IDENT");
let (name, email) = emails::signature(ctx, &which, &me.did, &me.handle).await;
git.envs.push((format!("GIT_{who}_NAME"), name));
git.envs.push((format!("GIT_{who}_EMAIL"), email));
}
git
}
async fn merge_commit(
ctx: &Ctx,
git: &Git,
tree: &str,
base: &str,
@@ -3432,7 +3433,8 @@ async fn merge_commit(
me: &Session,
) -> Result<String> {
let message = format!("Merge {}\n\n{}", item.title, item.uri);
committer(git, me)
committer(ctx, git, me)
.await
.text(
&["commit-tree", tree, "-p", base, "-p", head, "-m", &message],
None,
@@ -3443,6 +3445,7 @@ async fn merge_commit(
/// The pull as one commit on `base`: the head's author wrote it, `me`
/// commits it, and the message is the pull's.
pub(crate) async fn squash_commit(
ctx: &Ctx,
git: &Git,
tree: &str,
base: &str,
@@ -3450,7 +3453,7 @@ pub(crate) async fn squash_commit(
item: &Item,
me: &Session,
) -> Result<String> {
let mut git = committer(git, me);
let mut git = committer(ctx, git, me).await;
let author = git
.text(&["log", "-1", "--format=%an%x00%ae%x00%aI", head], None)
.await?;
crates/knotless/src/queue.rs+8−2
@@ -1022,7 +1022,13 @@ impl Stack {
/// commit of its own on the one before, committed by `me`: as the merge
/// queue lands them, one commit per pull. Each that does not apply on what
/// is before it is left out.
pub async fn stack(git: &Git, base: &str, pulls: &[(Item, String)], me: &Session) -> Result<Stack> {
pub async fn stack(
ctx: &Ctx,
git: &Git,
base: &str,
pulls: &[(Item, String)],
me: &Session,
) -> Result<Stack> {
let mut stack = Stack {
base: base.to_string(),
stacked: Vec::new(),
@@ -1033,7 +1039,7 @@ pub async fn stack(git: &Git, base: &str, pulls: &[(Item, String)], me: &Session
let since = forge::since_of(git, pull, &tip, head).await?;
match forge::merge_tree_from(git, since.as_deref(), &tip, head).await? {
Ok(tree) => {
let commit = forge::squash_commit(git, &tree, &tip, head, pull, me).await?;
let commit = forge::squash_commit(ctx, git, &tree, &tip, head, pull, me).await?;
let (pull, head) = (pull.clone(), head.clone());
stack.stacked.push(Stacked { pull, head, commit });
}
crates/knotless/src/revert.rs+1−1
@@ -66,7 +66,7 @@ pub async fn revert(
let key = t.snap.key().expect("an opened repository has a key");
let mirror = Mirror::of(ctx, &t.remote, key).await?;
mirror.sync(&t.remote, &t.snap).await?;
let git = forge::committer(&mirror.git, &me);
let git = forge::committer(ctx, &mirror.git, &me).await;
if tip.is_empty() || !git.is_ancestor(merged, &tip).await? {
bail!("{base} no longer holds the merge of {pull}, {merged}");
}
crates/knotless/tests/cli.rs+21−2
@@ -323,6 +323,25 @@ fn repository_lifecycle() {
s.git(&clone, &["log", "-1", "--format=%an", "HEAD~1"]),
"Ada"
);
// Named by no one, the author is the account, as a server commits for
// it; the binary, on a person's own machine, commits as git does there.
let did = &s.account.did;
let by = s.git(&clone, &["log", "-1", "--format=%an <%ae>", "HEAD"]);
assert_eq!(by, format!("{} <{did}@knotless.invalid>", s.account.handle));
let put = ["--put", "git.txt", file.to_str().unwrap()];
let args = [
&["repo", "commit", &url, "-b", "trunk", "-m", "git's"][..],
&put,
]
.concat();
let (done, _, err) = s.knotless(&args, b"");
assert!(done, "{err}");
s.git(&clone, &["fetch", "-q", "origin"]);
let by = s.git(
&clone,
&["log", "-1", "--format=%an <%ae>|%cn <%ce>", "origin/trunk"],
);
assert_eq!(by, "Test <t@example.com>|Test <t@example.com>");
assert!(!clone.join("run.sh").exists());
// The remote remembers whose it is, and will not follow its handle to
// another account.
@@ -340,12 +359,12 @@ fn repository_lifecycle() {
// Asked as a push asks, it only counts until there are enough packs.
assert_eq!(
s.cli(&["repo", "compact", &url, "--due"], b"").1,
"2 packs, 0 folded, 0 old claims dropped, 0 deleted refs' records dropped"
"3 packs, 0 folded, 0 old claims dropped, 0 deleted refs' records dropped"
);
assert_ne!(s.cli(&["repo", "compact", &url, "--soon"], b"").0, 0);
assert_eq!(
s.cli(&["repo", "compact", &url], b"").1,
"2 packs, 2 folded, 0 old claims dropped, 0 deleted refs' records dropped"
"3 packs, 3 folded, 0 old claims dropped, 0 deleted refs' records dropped"
);
// Folded, its history is one pack, small enough to ride inline.
let (_, storage) = s.cli(&["repo", "storage", &url], b"");
crates/knotless-mcp/src/main.rs+4−1
crates/knotless-run/src/lib.rs+1−1
crates/knotless-run/src/main.rs+3−1
crates/knotless-run/src/queue.rs+1−1
crates/knotless-run/tests/run.rs+1−1
crates/knotless-serve/tests/forge.rs+6−4
crates/knotless-serve/tests/gateway.rs+14−4
docs/design/collaboration.md+3−2
docs/design/gateway.md+4−4
docs/design/identity.md+19−2
docs/design/merge-queue.md+3−2
docs/design/security.md+2−1
docs/design/storage.md+4−2
docs/reference/cli.md+1−1
docs/reference/workflows.md+1−1