Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 1 | //! Statuses on commits: what workflow runs (and other tools, such as |
| 2 | //! deployments) say about a pull request's head. These are its checks. | |
| 3 | //! | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 4 | //! The rules of the branch a pull request merges into name the checks that |
| 5 | //! must pass (rulesets.rs; `RepoSettings::required_checks` as they stack): | |
| 6 | //! a required check that failed, is still running or has not reported | |
| 7 | //! refuses the merge, for everyone and for the merge queue (`g1t_rules` | |
| 8 | //! says so). Where g1t sees an agent's pull request through, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 9 | //! any check that failed sends the agent back to fix it, with what the |
| 10 | //! failing jobs printed; once it is out of revisions, only a required | |
| 11 | //! check holds the pull request for a person. | |
| The keeper reads Cloudflare as it really answers | 12 | |
| A mirror of a GitHub repository whose workflows g1t runs, in CI failover, during a takeover or with CI kept warm, now shows their results on GitHub: each status g1t sets on a commit, a workflow run's or a deploy's, goes to the commit there through g1t's GitHub App as a commit status named under g1t / (g1t / CI / push; g1t / deploy stays as it is), with its state, its description and a link back to g1t, once each time it changes, and commits made on g1t during a takeover are skipped. It is on by default and turned off with Report checks to GitHub in the mirror's settings, or report_checks through the API. The app needs the Commit statuses permission; until an installation has it, the mirror's settings say g1t can't report checks to GitHub and why, and the line goes once a status gets through. Statuses set by workflow runs, deploys and g1t's other parts are now published as status.created, as ones set through the API were, so webhooks hear them too. The mirroring, actions, GitHub, checks, webhooks and self-hosting guides say so. | 13 | use g1t_contracts::checks::StatusEvent; |
| The keeper reads Cloudflare as it really answers | 14 | use g1t_contracts::events::ChecksEvent; |
| 15 | use g1t_contracts::time::rfc3339; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 16 | use g1t_contracts::work::{ |
| 17 | CommitStatus, RequiredCheck, RequiredState, SeenCheck, SeenChecksArgs, SetCommitStatusArgs, check_name, | |
| 18 | required_checks, | |
| 19 | }; | |
| The keeper reads Cloudflare as it really answers | 20 | use g1t_contracts::{FailureCode, Outcome}; |
| 21 | use g1t_kit::now_ms; | |
| 22 | use serde::Deserialize; | |
| 23 | use worker::Result; | |
| 24 | ||
| 25 | use crate::Work; | |
| 26 | ||
| 27 | #[derive(Deserialize)] | |
| 28 | struct StatusRow { | |
| 29 | context: String, | |
| 30 | state: String, | |
| 31 | description: Option<String>, | |
| 32 | target_url: Option<String>, | |
| 33 | updated_at: String, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 34 | #[serde(default)] |
| 35 | source: Option<String>, | |
| Merge checks: statuses and check runs on every commit | 36 | #[serde(default)] |
| 37 | check_run_id: Option<String>, | |
| The keeper reads Cloudflare as it really answers | 38 | } |
| 39 | ||
| 40 | impl From<StatusRow> for CommitStatus { | |
| 41 | fn from(row: StatusRow) -> Self { | |
| 42 | CommitStatus { | |
| 43 | context: row.context, | |
| 44 | state: row.state, | |
| 45 | description: row.description, | |
| 46 | target_url: row.target_url, | |
| 47 | updated_at: row.updated_at, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 48 | source: row.source, |
| Merge checks: statuses and check runs on every commit | 49 | check_run_id: row.check_run_id, |
| The keeper reads Cloudflare as it really answers | 50 | } |
| 51 | } | |
| 52 | } | |
| 53 | ||
| 54 | #[derive(Deserialize)] | |
| 55 | struct HeadRow { | |
| 56 | id: String, | |
| 57 | number: u32, | |
| 58 | } | |
| 59 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 60 | /// What a commit's checks say: every status still running and every one |
| 61 | /// that failed, by context, and where each required check stands. | |
| 62 | #[derive(Clone, Debug, Default, PartialEq)] | |
| The keeper reads Cloudflare as it really answers | 63 | pub(crate) struct WorkflowFacts { |
| 64 | pub(crate) pending: Vec<String>, | |
| 65 | pub(crate) failed: Vec<String>, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 66 | pub(crate) required: Vec<RequiredCheck>, |
| The keeper reads Cloudflare as it really answers | 67 | } |
| 68 | ||
| 69 | impl WorkflowFacts { | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 70 | /// `required` names the checks the default branch's protection requires. |
| 71 | pub(crate) fn of(statuses: &[CommitStatus], required: &[String]) -> WorkflowFacts { | |
| The keeper reads Cloudflare as it really answers | 72 | WorkflowFacts { |
| 73 | pending: statuses.iter().filter(|s| s.state == "pending").map(|s| s.context.clone()).collect(), | |
| 74 | failed: statuses.iter().filter(|s| s.state == "failure" || s.state == "error").map(|s| s.context.clone()).collect(), | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 75 | required: required_checks(required, statuses), |
| The keeper reads Cloudflare as it really answers | 76 | } |
| 77 | } | |
| 78 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 79 | fn required_in(&self, state: RequiredState) -> Vec<String> { |
| 80 | self.required.iter().filter(|check| check.state == state).map(|check| check.name.clone()).collect() | |
| 81 | } | |
| 82 | ||
| 83 | /// The required checks that failed, by name. | |
| 84 | pub(crate) fn required_failed(&self) -> Vec<String> { | |
| 85 | self.required_in(RequiredState::Failure) | |
| 86 | } | |
| 87 | ||
| 88 | /// The required checks nothing has reported on the commit yet. | |
| 89 | pub(crate) fn expected(&self) -> Vec<String> { | |
| 90 | self.required_in(RequiredState::Expected) | |
| 91 | } | |
| 92 | } | |
| 93 | ||
| 94 | /// The check names in `(context, last reported)` rows, most recent first: | |
| 95 | /// each name once, with the events it was reported for. | |
| 96 | pub(crate) fn seen(rows: Vec<(String, String)>) -> Vec<SeenCheck> { | |
| 97 | let mut rows = rows; | |
| 98 | rows.sort_by(|a, b| b.1.cmp(&a.1)); | |
| 99 | let mut out: Vec<SeenCheck> = Vec::new(); | |
| 100 | for (context, at) in rows { | |
| 101 | let (name, event) = check_name(&context); | |
| 102 | match out.iter_mut().find(|seen| seen.name.eq_ignore_ascii_case(name)) { | |
| 103 | Some(seen) => { | |
| 104 | if let Some(event) = event | |
| 105 | && !seen.events.iter().any(|known| known == event) | |
| 106 | { | |
| 107 | seen.events.push(event.to_owned()); | |
| 108 | } | |
| 109 | } | |
| 110 | None => out.push(SeenCheck { | |
| 111 | name: name.to_owned(), | |
| 112 | events: event.map(|event| vec![event.to_owned()]).unwrap_or_default(), | |
| 113 | last_seen: at, | |
| 114 | }), | |
| 115 | } | |
| 116 | } | |
| 117 | out | |
| 118 | } | |
| 119 | ||
| 120 | /// How far back `seen_checks` looks, and the most contexts it reads. | |
| 121 | const SEEN_DAYS: u64 = 30; | |
| 122 | const SEEN_LIMIT: u32 = 200; | |
| 123 | ||
| 124 | #[derive(Deserialize)] | |
| 125 | struct SeenRow { | |
| 126 | context: String, | |
| 127 | at: String, | |
| 128 | } | |
| 129 | ||
| The keeper reads Cloudflare as it really answers | 130 | pub(crate) fn list(names: &[String]) -> String { |
| 131 | match names { | |
| 132 | [] => String::new(), | |
| 133 | [one] => one.clone(), | |
| 134 | [rest @ .., last] => format!("{} and {last}", rest.join(", ")), | |
| 135 | } | |
| 136 | } | |
| 137 | ||
| 138 | impl Work { | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 139 | /// Where a commit's checks stand, against the repository's required ones. |
| 140 | pub(crate) async fn facts(&self, repo_id: &str, sha: Option<&str>) -> Result<WorkflowFacts> { | |
| 141 | let (statuses, settings) = | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 142 | futures_util::future::try_join(self.statuses(repo_id, sha), self.settings_by_id(repo_id)).await?; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 143 | Ok(WorkflowFacts::of(&statuses, &settings.required_checks)) |
| 144 | } | |
| 145 | ||
| 146 | /// The check names reported on a repository's commits lately, for | |
| 147 | /// choosing which to require. | |
| 148 | pub(crate) async fn seen_checks(&self, a: SeenChecksArgs) -> Result<Outcome<Vec<SeenCheck>>> { | |
| 149 | let repo = match self.repo(&a.repo, &a.viewer).await? { | |
| 150 | Outcome::Ok(repo) => repo, | |
| 151 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 152 | }; | |
| 153 | let since = rfc3339(now_ms().saturating_sub(SEEN_DAYS * 24 * 60 * 60 * 1000)); | |
| 154 | let rows = self | |
| 155 | .db | |
| 156 | .prepare( | |
| 157 | "SELECT context, MAX(updated_at) AS at FROM commit_statuses | |
| 158 | WHERE repo_id = ? AND updated_at >= ? GROUP BY context ORDER BY at DESC LIMIT ?", | |
| 159 | ) | |
| 160 | .bind(&[repo.id.as_str().into(), since.into(), SEEN_LIMIT.into()])? | |
| 161 | .all() | |
| 162 | .await? | |
| 163 | .results::<SeenRow>()?; | |
| 164 | Ok(Outcome::Ok(seen(rows.into_iter().map(|row| (row.context, row.at)).collect()))) | |
| 165 | } | |
| 166 | ||
| The keeper reads Cloudflare as it really answers | 167 | pub(crate) async fn statuses(&self, repo_id: &str, sha: Option<&str>) -> Result<Vec<CommitStatus>> { |
| 168 | let Some(sha) = sha else { return Ok(Vec::new()) }; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 169 | if let Some(found) = self.prefetched_repo(repo_id).filter(|found| found.head.as_deref() == Some(sha)) { |
| 170 | return Ok(found | |
| 171 | .rows::<StatusRow>(crate::prefetch::Slot::Statuses)? | |
| 172 | .into_iter() | |
| 173 | .map(CommitStatus::from) | |
| 174 | .collect()); | |
| 175 | } | |
| The keeper reads Cloudflare as it really answers | 176 | Ok(self |
| 177 | .db | |
| Merge checks: statuses and check runs on every commit | 178 | .prepare("SELECT context, state, description, target_url, updated_at, source, check_run_id FROM commit_statuses WHERE repo_id = ? AND sha = ? ORDER BY context") |
| The keeper reads Cloudflare as it really answers | 179 | .bind(&[repo_id.into(), sha.into()])? |
| 180 | .all() | |
| 181 | .await? | |
| 182 | .results::<StatusRow>()? | |
| 183 | .into_iter() | |
| 184 | .map(CommitStatus::from) | |
| 185 | .collect()) | |
| 186 | } | |
| 187 | ||
| A mirror of a GitHub repository whose workflows g1t runs, in CI failover, during a takeover or with CI kept warm, now shows their results on GitHub: each status g1t sets on a commit, a workflow run's or a deploy's, goes to the commit there through g1t's GitHub App as a commit status named under g1t / (g1t / CI / push; g1t / deploy stays as it is), with its state, its description and a link back to g1t, once each time it changes, and commits made on g1t during a takeover are skipped. It is on by default and turned off with Report checks to GitHub in the mirror's settings, or report_checks through the API. The app needs the Commit statuses permission; until an installation has it, the mirror's settings say g1t can't report checks to GitHub and why, and the line goes once a status gets through. Statuses set by workflow runs, deploys and g1t's other parts are now published as status.created, as ones set through the API were, so webhooks hear them too. The mirroring, actions, GitHub, checks, webhooks and self-hosting guides say so. | 188 | /// A status from another part of g1t: a workflow run, a deploy, a scan. |
| 189 | /// Published as `status.created`, as one set through the API is, so | |
| 190 | /// webhooks hear it and a GitHub mirror's commit is told (integrations). | |
| 191 | /// Publishing is best effort: the status is kept either way. | |
| The keeper reads Cloudflare as it really answers | 192 | pub(crate) async fn set_commit_status(&self, a: SetCommitStatusArgs) -> Result<Outcome<bool>> { |
| A mirror of a GitHub repository whose workflows g1t runs, in CI failover, during a takeover or with CI kept warm, now shows their results on GitHub: each status g1t sets on a commit, a workflow run's or a deploy's, goes to the commit there through g1t's GitHub App as a commit status named under g1t / (g1t / CI / push; g1t / deploy stays as it is), with its state, its description and a link back to g1t, once each time it changes, and commits made on g1t during a takeover are skipped. It is on by default and turned off with Report checks to GitHub in the mirror's settings, or report_checks through the API. The app needs the Commit statuses permission; until an installation has it, the mirror's settings say g1t can't report checks to GitHub and why, and the line goes once a status gets through. Statuses set by workflow runs, deploys and g1t's other parts are now published as status.created, as ones set through the API were, so webhooks hear them too. The mirroring, actions, GitHub, checks, webhooks and self-hosting guides say so. | 193 | let event = StatusEvent { |
| 194 | repo_id: a.repo_id.clone(), | |
| 195 | sha: a.sha.clone(), | |
| 196 | context: a.context.clone(), | |
| 197 | state: a.state.clone(), | |
| 198 | description: a.description.clone(), | |
| 199 | target_url: a.target_url.clone(), | |
| 200 | }; | |
| 201 | let repo_id = a.repo_id.clone(); | |
| 202 | let stored = self.store_status(a, None).await?; | |
| 203 | if matches!(stored, Outcome::Ok(_)) | |
| 204 | && let Err(error) = self.publish_as("status.created", &repo_id, None, event).await | |
| 205 | { | |
| 206 | worker::console_error!("status.created not published: {error}"); | |
| 207 | } | |
| 208 | Ok(stored) | |
| Merge checks: statuses and check runs on every commit | 209 | } |
| 210 | ||
| 211 | /// Sets a status, standing for check run `check_run_id` if it does, | |
| 212 | /// and moves on what was waiting for the commit's checks. | |
| 213 | pub(crate) async fn store_status(&self, a: SetCommitStatusArgs, check_run_id: Option<&str>) -> Result<Outcome<bool>> { | |
| The keeper reads Cloudflare as it really answers | 214 | if !matches!(a.state.as_str(), "pending" | "success" | "failure" | "error") { |
| 215 | return Ok(Outcome::fail(FailureCode::Invalid, "`state` is pending, success, failure or error.")); | |
| 216 | } | |
| 217 | self.db | |
| 218 | .prepare( | |
| Merge checks: statuses and check runs on every commit | 219 | "INSERT INTO commit_statuses (repo_id, sha, context, state, description, target_url, updated_at, source, check_run_id) |
| 220 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| The keeper reads Cloudflare as it really answers | 221 | ON CONFLICT (repo_id, sha, context) DO UPDATE SET |
| 222 | state = excluded.state, description = excluded.description, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 223 | target_url = excluded.target_url, updated_at = excluded.updated_at, |
| Merge checks: statuses and check runs on every commit | 224 | source = excluded.source, check_run_id = excluded.check_run_id", |
| The keeper reads Cloudflare as it really answers | 225 | ) |
| 226 | .bind(&[ | |
| 227 | a.repo_id.as_str().into(), | |
| 228 | a.sha.as_str().into(), | |
| 229 | a.context.as_str().into(), | |
| 230 | a.state.as_str().into(), | |
| 231 | a.description.as_deref().map_or(worker::wasm_bindgen::JsValue::NULL, Into::into), | |
| 232 | a.target_url.as_deref().map_or(worker::wasm_bindgen::JsValue::NULL, Into::into), | |
| 233 | rfc3339(now_ms()).into(), | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 234 | a.source.as_deref().map_or(worker::wasm_bindgen::JsValue::NULL, Into::into), |
| Merge checks: statuses and check runs on every commit | 235 | check_run_id.map_or(worker::wasm_bindgen::JsValue::NULL, Into::into), |
| The keeper reads Cloudflare as it really answers | 236 | ])? |
| 237 | .run() | |
| 238 | .await?; | |
| 239 | if a.state == "pending" { | |
| 240 | return Ok(Outcome::Ok(true)); | |
| 241 | } | |
| 242 | // Once every workflow on a pull request's head has finished, its | |
| 243 | // lifecycle moves on, as it does when its checks finish. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 244 | let facts = self.facts(&a.repo_id, Some(&a.sha)).await?; |
| The keeper reads Cloudflare as it really answers | 245 | if !facts.pending.is_empty() { |
| 246 | return Ok(Outcome::Ok(true)); | |
| 247 | } | |
| 248 | // A merge queue state waiting on its merge_group workflows. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 249 | self.merge_group_finished(&a.repo_id, &a.sha, &facts).await?; |
| The keeper reads Cloudflare as it really answers | 250 | let heads = self |
| 251 | .db | |
| 252 | .prepare("SELECT id, number FROM pulls WHERE repo_id = ? AND head_commit = ? AND status IN ('draft', 'open')") | |
| 253 | .bind(&[a.repo_id.as_str().into(), a.sha.as_str().into()])? | |
| 254 | .all() | |
| 255 | .await? | |
| 256 | .results::<HeadRow>()?; | |
| 257 | for head in heads { | |
| 258 | // A pull request g1t stopped on picks back up once what stopped | |
| 259 | // it passes: its workflows, and its checks if it has any. The | |
| 260 | // lifecycle then decides again, within its usual limits. | |
| 261 | if facts.failed.is_empty() { | |
| 262 | let resumed = self | |
| 263 | .db | |
| 264 | .prepare( | |
| 265 | "UPDATE pulls SET stalled = NULL WHERE id = ? AND managed = 1 AND stalled IS NOT NULL | |
| 266 | AND (check_status IS NULL OR check_status = 'passed') RETURNING id AS value", | |
| 267 | ) | |
| 268 | .bind(&[head.id.as_str().into()])? | |
| 269 | .first::<crate::rows::ValueRow>(None) | |
| 270 | .await?; | |
| 271 | if resumed.is_some() { | |
| Events: review requests, assignments, stops and deployments are published | 272 | self.announce_resumed(&head.id, None).await?; |
| The keeper reads Cloudflare as it really answers | 273 | self.note( |
| 274 | &a.repo_id, | |
| 275 | head.number, | |
| 276 | (crate::lifecycle::POLICY_ACTOR_ID, crate::lifecycle::POLICY_ACTOR_NAME), | |
| 277 | "picked this back up: its workflows pass now", | |
| 278 | ) | |
| 279 | .await?; | |
| 280 | } | |
| 281 | } | |
| 282 | self.publish_as( | |
| 283 | "checks.completed", | |
| 284 | &a.repo_id, | |
| 285 | None, | |
| 286 | ChecksEvent { | |
| 287 | pull_id: head.id, | |
| 288 | repo_id: a.repo_id.clone(), | |
| 289 | number: head.number, | |
| 290 | status: if facts.failed.is_empty() { "passed" } else { "failed" }, | |
| 291 | commit: a.sha.clone(), | |
| 292 | }, | |
| 293 | ) | |
| 294 | .await?; | |
| 295 | } | |
| 296 | Ok(Outcome::Ok(true)) | |
| 297 | } | |
| 298 | } | |
| 299 | ||
| 300 | #[cfg(test)] | |
| 301 | mod tests { | |
| 302 | use super::*; | |
| 303 | ||
| 304 | fn status(context: &str, state: &str) -> CommitStatus { | |
| 305 | CommitStatus { | |
| 306 | context: context.into(), | |
| 307 | state: state.into(), | |
| 308 | description: None, | |
| 309 | target_url: None, | |
| 310 | updated_at: String::new(), | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 311 | source: None, |
| Merge checks: statuses and check runs on every commit | 312 | check_run_id: None, |
| The keeper reads Cloudflare as it really answers | 313 | } |
| 314 | } | |
| 315 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 316 | fn names(list: &[&str]) -> Vec<String> { |
| 317 | list.iter().map(|name| (*name).to_owned()).collect() | |
| 318 | } | |
| 319 | ||
| 320 | #[test] | |
| 321 | fn only_required_checks_hold_a_merge() { | |
| 322 | let statuses = [status("CI / push", "pending"), status("Lint / pull_request", "failure"), status("Docs", "success")]; | |
| 323 | // Nothing required: nothing holds it, whatever failed. | |
| 324 | let free = WorkflowFacts::of(&statuses, &[]); | |
| 325 | assert_eq!(free.pending, ["CI / push"]); | |
| 326 | assert_eq!(free.failed, ["Lint / pull_request"]); | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 327 | assert!(free.required_failed().is_empty() && free.expected().is_empty()); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 328 | let both = WorkflowFacts::of(&statuses, &names(&["CI", "Lint"])); |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 329 | assert_eq!(both.required_failed(), ["Lint"]); |
| 330 | assert!(WorkflowFacts::of(&[status("Docs", "success")], &names(&["Docs"])).required_failed().is_empty()); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 331 | } |
| 332 | ||
| 333 | #[test] | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 334 | fn a_required_check_nothing_reported_is_expected() { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 335 | let facts = WorkflowFacts::of(&[status("CI / pull_request", "success")], &names(&["CI", "Deploy"])); |
| 336 | assert_eq!(facts.expected(), ["Deploy"]); | |
| 337 | } | |
| 338 | ||
| The keeper reads Cloudflare as it really answers | 339 | #[test] |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 340 | fn seen_checks_are_named_once_with_their_events() { |
| 341 | let rows = vec![ | |
| 342 | ("CI / push".to_owned(), "2026-10-01T00:00:00Z".to_owned()), | |
| 343 | ("CI / pull_request".to_owned(), "2026-10-03T00:00:00Z".to_owned()), | |
| 344 | ("g1t / deploy".to_owned(), "2026-10-02T00:00:00Z".to_owned()), | |
| 345 | ]; | |
| 346 | let seen = seen(rows); | |
| 347 | assert_eq!(seen.len(), 2); | |
| 348 | assert_eq!(seen[0].name, "CI"); | |
| 349 | assert_eq!(seen[0].events, ["pull_request", "push"]); | |
| 350 | assert_eq!(seen[0].last_seen, "2026-10-03T00:00:00Z"); | |
| 351 | assert_eq!(seen[1].name, "g1t / deploy"); | |
| 352 | assert!(seen[1].events.is_empty()); | |
| The keeper reads Cloudflare as it really answers | 353 | } |
| 354 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.