use std::collections::BTreeMap;
use crate::config::schema::GithubConfig;
use crate::sandbox::backend::{AGENT_BIN, STATE_PATH, WORKSPACE_PATH};
pub const TOKEN_VARIABLE: &str = "GH_TOKEN";
pub const GITCONFIG_FILENAME: &str = ".gitconfig";
pub const REQUEST_FILENAME: &str = "pull-request.txt";
pub const ASKED_FILENAME: &str = "pull-request-asked";
pub const GH_SHIM_FILENAME: &str = "gh";
pub fn gh_shim_contents() -> String {
[
"#!/bin/sh",
"# Generated per session by errand. Do not edit.",
"if [ \"$1\" = \"pr\" ] && [ \"$2\" = \"create\" ]; then",
" echo \"errand: pull requests here are opened by the daemon, not by gh.\" >&2",
&format!(
" echo \"Write the title to {STATE_PATH}/{REQUEST_FILENAME} and it opens when your turn ends.\" >&2"
),
" exit 1",
"fi",
"IFS=:",
"for dir in $PATH; do",
&format!(
" if [ \"$dir\" != \"{AGENT_BIN}\" ] && [ -x \"$dir/{GH_SHIM_FILENAME}\" ]; then"
),
&format!(" exec \"$dir/{GH_SHIM_FILENAME}\" \"$@\""),
" fi",
"done",
"echo \"errand: gh is not installed\" >&2",
"exit 127",
"",
]
.join("\n")
}
pub fn git_config_contents(github: &GithubConfig) -> String {
[
"# Generated per session by errand. Do not edit.",
"[user]",
&format!("\tname = {}", github.user_name),
&format!("\temail = {}", github.user_email),
"",
"[credential \"https://github.com\"]",
"\thelper = !gh auth git-credential",
"",
]
.join("\n")
}
pub fn git_identity_env(github: &GithubConfig) -> BTreeMap<String, String> {
BTreeMap::from([
("GIT_AUTHOR_NAME".to_owned(), github.user_name.clone()),
("GIT_AUTHOR_EMAIL".to_owned(), github.user_email.clone()),
("GIT_COMMITTER_NAME".to_owned(), github.user_name.clone()),
("GIT_COMMITTER_EMAIL".to_owned(), github.user_email.clone()),
])
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct SessionLinks {
pub thread: Option<String>,
pub transcript: Option<String>,
}
pub fn attribution_footer(requested_by: &str, links: &SessionLinks) -> String {
let mut said = vec![format!("Requested by {requested_by} via errand.")];
if let Some(thread) = &links.thread {
said.push(format!("Conversation: {thread}"));
}
if let Some(transcript) = &links.transcript {
said.push(format!("Transcript: {transcript}"));
}
said.join("\n")
}
pub fn thread_link(guild_id: &str, thread_id: &str) -> String {
format!("https://discord.com/channels/{guild_id}/{thread_id}")
}
pub fn transcript_link(public_url: &str, session_id: &str) -> String {
let base = public_url.trim_end_matches('/');
format!("{base}/?session={}", urlencoding_escape(session_id))
}
fn urlencoding_escape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for byte in text.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char);
}
_ => {
use std::fmt::Write as _;
let _ = write!(out, "%{byte:02X}");
}
}
}
out
}
pub fn review_instructions(
github: &GithubConfig,
requested_by: &str,
links: &SessionLinks,
) -> String {
[
"",
"## Opening a pull request",
"",
"`gh` is authenticated, so read issues, leave comments and check builds as",
"you would anywhere. Two things are different.",
"",
&format!(
"Commit as `{} <{}>`, which is already set up.",
github.user_name, github.user_email
),
"Do not pass `-c user.name`, `-c user.email` or `--author`, and do not set",
"`GIT_AUTHOR_NAME` or `GIT_COMMITTER_NAME`. The commits belong to the bot",
"account that opens the pull request, not to you.",
"",
"**Open nothing unless you were asked to.** Committing your work to a",
"branch is the whole job unless somebody in the thread asks for a pull",
"request. Never run `gh pr create`, and never ask for one unprompted.",
"",
"When you are asked: commit to a branch of its own, with a message saying",
"what changed and why, then write the title you want on the first line of",
&format!("`{STATE_PATH}/{REQUEST_FILENAME}`. If you have cloned more than one repository"),
&format!(
"into {WORKSPACE_PATH}, add a line of `repository: <directory name>` so the right"
),
"one is opened. Write the file once, when the work is finished.",
"",
"**That file is a request, not the result.** The pull request is opened",
"after your turn ends, and the thread is told whether it worked and where",
"it went. You will not have seen that outcome, so do not say a pull request",
"is open and do not quote an address for one.",
"",
"The daemon opens it so the description ends with the lines below, which",
"are how a repository receiving work from a bot can tell where it came",
"from. If you ever open one yourself despite the above, end the description",
"with exactly these lines, after a `---` rule. Copy them literally. Never",
"write an `@` mention: the name is a chat name, and the GitHub account that",
"happens to match it belongs to somebody who asked for nothing.",
"",
"```",
&attribution_footer(requested_by, links),
"```",
"",
]
.join("\n")
}
#[cfg(test)]
mod tests;