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.
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1 | //! A repository's About, as its Files page shows it beside the files: what |
| 2 | //! its files say about it (its license, its security policy and the | |
| 3 | //! languages it is written in), who made it (its contributors), who starred | |
| 4 | //! it, and its releases. Methods of the repos service. | |
| 5 | //! | |
| 6 | //! What is read from history and files is worked out in the background for | |
| 7 | //! the default branch's head and kept by commit (services/repos/src/stats.rs), | |
| 8 | //! never on the way to a page: an answer can be for an older commit | |
| 9 | //! (`commit` is not `head`) while the newer one is worked out, or `pending` | |
| 10 | //! when nothing has been worked out yet. | |
| 11 | ||
| 12 | use serde::{Deserialize, Serialize}; | |
| 13 | ||
| 14 | use crate::repos::{RepoPath, Repo}; | |
| 15 | use crate::{User, Viewer}; | |
| 16 | ||
| 17 | /// One language's share of a repository's code, by bytes. | |
| 18 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 19 | #[serde(rename_all = "camelCase")] | |
| 20 | pub struct LanguageShare { | |
| 21 | pub name: String, | |
| 22 | /// `#rrggbb`, the color it is known by; null for one without. | |
| 23 | pub color: Option<String>, | |
| 24 | pub bytes: u64, | |
| 25 | /// Of the bytes counted, to one decimal place. | |
| 26 | pub percent: f64, | |
| 27 | } | |
| 28 | ||
| 29 | /// The license a repository's LICENSE (or COPYING) file holds. | |
| 30 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 31 | #[serde(rename_all = "camelCase")] | |
| 32 | pub struct License { | |
| 33 | /// Its SPDX identifier, such as `MIT` or `Apache-2.0`; null when the | |
| 34 | /// text is not one g1t recognizes. | |
| 35 | pub spdx_id: Option<String>, | |
| 36 | /// What people call it: "MIT License", or "Other" when unrecognized. | |
| 37 | pub name: String, | |
| 38 | /// The file it was read from, from the root: `LICENSE`. | |
| 39 | pub path: String, | |
| 40 | } | |
| 41 | ||
| 42 | /// Who a contributor is: a person with an account (their commits' address | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 43 | /// is one they confirmed), g1t itself, a known coding agent or bot by the |
| 44 | /// address on its commits (`coding_agents`), or an author g1t cannot match | |
| 45 | /// to anyone. | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 46 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 47 | #[serde(rename_all = "snake_case")] | |
| 48 | pub enum ContributorKind { | |
| 49 | User, | |
| 50 | G1t, | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 51 | Agent, |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 52 | Author, |
| 53 | } | |
| 54 | ||
| 55 | /// Commits in one week, the week named by its Monday (`YYYY-MM-DD`, UTC). | |
| 56 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 57 | pub struct WeekCommits { | |
| 58 | pub week: String, | |
| 59 | pub commits: u32, | |
| 60 | } | |
| 61 | ||
| 62 | /// Someone whose commits are on the default branch. | |
| 63 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 64 | #[serde(rename_all = "camelCase")] | |
| 65 | pub struct Contributor { | |
| 66 | pub kind: ContributorKind, | |
| 67 | /// Their username for a `user` (and `g1t` for g1t); the name on their | |
| 68 | /// commits otherwise. | |
| 69 | pub name: String, | |
| 70 | /// The account's username, for a `user`. | |
| 71 | #[serde(default)] | |
| 72 | pub username: Option<String>, | |
| 73 | /// The uploaded avatar's hash, for a `user` who has one. | |
| 74 | #[serde(default)] | |
| 75 | pub avatar: Option<String>, | |
| Coding agents show as themselves on commits and contributors: Claude, Codex, Copilot, Gemini, Jules, Cursor, Devin, Aider, Dependabot, Renovate and GitHub Actions are known by the address they write on their commits (never by name), shown with their maker's mark, their own name and an agent or bot label, and a card saying what each is. Anyone else not on g1t gets a card too, saying so and how to claim the commits by adding the address. Contributor lists are worked out again (stats version 2) so an agent counts once. | 76 | /// The coding agent's id (`coding_agents`), for an `agent`. |
| 77 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 78 | pub agent: Option<String>, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 79 | pub commits: u32, |
| 80 | /// RFC 3339: their first and latest commit read. | |
| 81 | pub first_at: String, | |
| 82 | pub last_at: String, | |
| 83 | /// Their commits by week, oldest first, the weeks with none left out. | |
| 84 | /// Only for the most active contributors (`MAX_CONTRIBUTOR_WEEKS` of | |
| 85 | /// them); empty for the rest and in the About summary. | |
| 86 | #[serde(default)] | |
| 87 | pub weeks: Vec<WeekCommits>, | |
| 88 | } | |
| 89 | ||
| 90 | /// The most contributors kept for a repository. | |
| 91 | pub const MAX_CONTRIBUTORS: usize = 500; | |
| 92 | /// The contributors whose commits are kept week by week. | |
| 93 | pub const MAX_CONTRIBUTOR_WEEKS: usize = 100; | |
| 94 | /// The contributors the About summary names. | |
| 95 | pub const ABOUT_CONTRIBUTORS: usize = 14; | |
| 96 | ||
| 97 | /// Where an answer worked out from the default branch stands. | |
| 98 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 99 | #[serde(rename_all = "camelCase")] | |
| 100 | pub struct Freshness { | |
| 101 | /// The default branch's head now; null for an empty repository. | |
| 102 | pub head: Option<String>, | |
| 103 | /// The commit the answer was worked out for; null when none has been. | |
| 104 | pub commit: Option<String>, | |
| 105 | /// RFC 3339: when it was. | |
| 106 | pub computed_at: Option<String>, | |
| 107 | /// Nothing has been worked out yet; it is under way. Ask again shortly. | |
| 108 | pub pending: bool, | |
| 109 | /// The history or the files were too large to read in full, so the | |
| 110 | /// answer counts what was read. | |
| 111 | pub partial: bool, | |
| 112 | } | |
| 113 | ||
| 114 | /// `languages`: a repository's languages by bytes, largest first. | |
| 115 | #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] | |
| 116 | #[serde(rename_all = "camelCase")] | |
| 117 | pub struct Languages { | |
| 118 | #[serde(flatten)] | |
| 119 | pub freshness: Freshness, | |
| 120 | pub languages: Vec<LanguageShare>, | |
| 121 | } | |
| 122 | ||
| 123 | /// `contributors`: everyone whose commits are on the default branch, most | |
| 124 | /// commits first, and the repository's commits by week. | |
| 125 | #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] | |
| 126 | #[serde(rename_all = "camelCase")] | |
| 127 | pub struct Contributors { | |
| 128 | #[serde(flatten)] | |
| 129 | pub freshness: Freshness, | |
| 130 | /// How many there are; `contributors` lists at most `MAX_CONTRIBUTORS`. | |
| 131 | pub total: u32, | |
| 132 | /// The commits read. | |
| 133 | pub commits: u32, | |
| 134 | pub contributors: Vec<Contributor>, | |
| 135 | /// Every commit read, by week, oldest first, including empty weeks. | |
| 136 | pub weeks: Vec<WeekCommits>, | |
| 137 | } | |
| 138 | ||
| 139 | /// A release: a tag, published with a title and notes. | |
| 140 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 141 | #[serde(rename_all = "camelCase")] | |
| 142 | pub struct Release { | |
| 143 | /// `rel_…`. | |
| 144 | pub id: String, | |
| 145 | pub tag_name: String, | |
| 146 | /// The commit the tag named when the release was made. | |
| 147 | pub target: String, | |
| 148 | /// Its title; the tag's name when it has none. | |
| 149 | pub name: Option<String>, | |
| 150 | /// Its notes, Markdown. | |
| 151 | pub body: String, | |
| 152 | /// Seen only by those who can push until it is published. | |
| 153 | pub draft: bool, | |
| 154 | /// Not ready for everyone: never the latest release. | |
| 155 | pub prerelease: bool, | |
| 156 | /// The username of who made it. | |
| 157 | pub author: Option<String>, | |
| 158 | /// RFC 3339. | |
| 159 | pub created_at: String, | |
| 160 | /// RFC 3339; null while it is a draft. | |
| 161 | pub published_at: Option<String>, | |
| 162 | /// Whether it is the latest release: the newest published one that is | |
| 163 | /// neither a draft nor a prerelease. | |
| 164 | #[serde(default)] | |
| 165 | pub latest: bool, | |
| 166 | } | |
| 167 | ||
| 168 | /// The longest release title, and the longest notes. | |
| 169 | pub const MAX_RELEASE_NAME_CHARS: usize = 200; | |
| 170 | pub const MAX_RELEASE_BODY_CHARS: usize = 125_000; | |
| 171 | ||
| 172 | /// Someone who starred a repository. | |
| 173 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 174 | #[serde(rename_all = "camelCase")] | |
| 175 | pub struct Stargazer { | |
| 176 | pub username: String, | |
| 177 | pub avatar: Option<String>, | |
| 178 | /// RFC 3339. | |
| 179 | pub starred_at: String, | |
| 180 | } | |
| 181 | ||
| 182 | /// A repository someone starred. | |
| 183 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 184 | #[serde(rename_all = "camelCase")] | |
| 185 | pub struct StarredRepo { | |
| 186 | pub repo: Repo, | |
| 187 | /// RFC 3339. | |
| 188 | pub starred_at: String, | |
| 189 | pub stars: u64, | |
| 190 | } | |
| 191 | ||
| 192 | /// Whether the viewer starred a repository, and how many have. | |
| 193 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 194 | #[serde(rename_all = "camelCase")] | |
| 195 | pub struct Stars { | |
| 196 | pub starred: bool, | |
| 197 | pub stars: u64, | |
| 198 | } | |
| 199 | ||
| 200 | /// The About of a repository's Files page, in one answer: what is kept for | |
| 201 | /// the default branch's head, the stars and the releases. | |
| 202 | #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] | |
| 203 | #[serde(rename_all = "camelCase")] | |
| 204 | pub struct RepoAbout { | |
| 205 | #[serde(flatten)] | |
| 206 | pub freshness: Freshness, | |
| 207 | pub license: Option<License>, | |
| 208 | /// The path of its security policy (SECURITY.md at the root, or in | |
| 209 | /// `.g1t`, `.github` or `docs`), when it has one. | |
| 210 | pub security_policy: Option<String>, | |
| 211 | pub languages: Vec<LanguageShare>, | |
| 212 | /// How many contributors there are, and the most active | |
| 213 | /// (`ABOUT_CONTRIBUTORS`), without their weeks. | |
| 214 | pub contributors: u32, | |
| 215 | pub top_contributors: Vec<Contributor>, | |
| 216 | pub stars: u64, | |
| 217 | pub starred: bool, | |
| 218 | /// Published releases the viewer can see, drafts left out unless they | |
| 219 | /// can push. | |
| 220 | pub releases: u64, | |
| 221 | pub latest_release: Option<Release>, | |
| 222 | } | |
| 223 | ||
| 224 | /// `about`, `languages`, `contributors`, `license`, `stars` and | |
| 225 | /// `releases`: one repository, for the viewer. `about` returns | |
| 226 | /// `Outcome<RepoAbout>`, `languages` `Outcome<Languages>`, `contributors` | |
| 227 | /// `Outcome<Contributors>`, `license` `Outcome<Option<License>>`, `stars` | |
| 228 | /// `Outcome<Stars>` and `releases` `Outcome<Vec<Release>>` (newest first, | |
| 229 | /// at most 100). | |
| 230 | #[derive(Debug, Serialize, Deserialize)] | |
| 231 | pub struct RepoViewArgs { | |
| 232 | pub path: RepoPath, | |
| 233 | pub viewer: Viewer, | |
| 234 | } | |
| 235 | ||
| 236 | /// `star`: stars the repository for the actor, or takes their star back. | |
| 237 | /// Returns `Outcome<Stars>`. | |
| 238 | #[derive(Debug, Serialize, Deserialize)] | |
| 239 | pub struct StarArgs { | |
| 240 | pub path: RepoPath, | |
| 241 | pub actor: User, | |
| 242 | pub starred: bool, | |
| 243 | } | |
| 244 | ||
| 245 | /// `stargazers`: who starred a repository, newest first, 100 a page; | |
| 246 | /// `page` from 1. Returns `Outcome<Vec<Stargazer>>`. | |
| 247 | #[derive(Debug, Serialize, Deserialize)] | |
| 248 | pub struct StargazersArgs { | |
| 249 | pub path: RepoPath, | |
| 250 | pub viewer: Viewer, | |
| 251 | #[serde(default)] | |
| 252 | pub page: Option<u32>, | |
| 253 | } | |
| 254 | ||
| 255 | /// `starred`: the repositories a person starred that the viewer can see, | |
| 256 | /// newest first, at most 100. The person by `username`. Returns | |
| 257 | /// `Vec<StarredRepo>`; empty for an unknown person. | |
| 258 | #[derive(Debug, Serialize, Deserialize)] | |
| 259 | pub struct StarredArgs { | |
| 260 | pub username: String, | |
| 261 | pub viewer: Viewer, | |
| 262 | } | |
| 263 | ||
| 264 | /// `release`: one release, by `id` or `tag`, or with `latest` the latest | |
| 265 | /// one. Returns `Outcome<Release>`. | |
| 266 | #[derive(Debug, Serialize, Deserialize)] | |
| 267 | pub struct ReleaseArgs { | |
| 268 | pub path: RepoPath, | |
| 269 | pub viewer: Viewer, | |
| 270 | #[serde(default)] | |
| 271 | pub id: Option<String>, | |
| 272 | #[serde(default)] | |
| 273 | pub tag: Option<String>, | |
| 274 | #[serde(default)] | |
| 275 | pub latest: bool, | |
| 276 | } | |
| 277 | ||
| 278 | /// `create_release`: a release of `tag_name`. A tag that does not exist | |
| 279 | /// yet is made at `target` (a branch or commit; the default branch when | |
| 280 | /// absent). Needs the Write role. Returns `Outcome<Release>`. | |
| 281 | #[derive(Debug, Serialize, Deserialize)] | |
| 282 | #[serde(rename_all = "camelCase")] | |
| 283 | pub struct CreateReleaseArgs { | |
| 284 | pub path: RepoPath, | |
| 285 | pub actor: User, | |
| 286 | pub tag_name: String, | |
| 287 | #[serde(default)] | |
| 288 | pub target: Option<String>, | |
| 289 | #[serde(default)] | |
| 290 | pub name: Option<String>, | |
| 291 | #[serde(default)] | |
| 292 | pub body: Option<String>, | |
| 293 | #[serde(default)] | |
| 294 | pub draft: bool, | |
| 295 | #[serde(default)] | |
| 296 | pub prerelease: bool, | |
| 297 | } | |
| 298 | ||
| 299 | /// `update_release`: changes whichever of a release's title, notes, draft | |
| 300 | /// and prerelease are given; an empty title clears it. Publishing a draft | |
| 301 | /// sets `published_at`. Needs the Write role. Returns `Outcome<Release>`. | |
| 302 | #[derive(Debug, Serialize, Deserialize)] | |
| 303 | pub struct UpdateReleaseArgs { | |
| 304 | pub path: RepoPath, | |
| 305 | pub actor: User, | |
| 306 | pub id: String, | |
| 307 | #[serde(default)] | |
| 308 | pub name: Option<String>, | |
| 309 | #[serde(default)] | |
| 310 | pub body: Option<String>, | |
| 311 | #[serde(default)] | |
| 312 | pub draft: Option<bool>, | |
| 313 | #[serde(default)] | |
| 314 | pub prerelease: Option<bool>, | |
| 315 | } | |
| 316 | ||
| 317 | /// `delete_release`: deletes a release; its tag stays. Needs the Write | |
| 318 | /// role. Returns `Outcome<bool>`. | |
| 319 | #[derive(Debug, Serialize, Deserialize)] | |
| 320 | pub struct DeleteReleaseArgs { | |
| 321 | pub path: RepoPath, | |
| 322 | pub actor: User, | |
| 323 | pub id: String, | |
| 324 | } | |
| 325 | ||
| 326 | /// Monday of the week `rfc3339` falls in (UTC), as `YYYY-MM-DD`; None when | |
| 327 | /// it cannot be read. | |
| 328 | pub fn week_of(rfc3339: &str) -> Option<String> { | |
| 329 | let days = days_from_civil_str(rfc3339.get(..10)?)?; | |
| 330 | // 1970-01-01 was a Thursday: day 0 is three days after a Monday. | |
| 331 | let monday = days - (days + 3).rem_euclid(7); | |
| 332 | Some(civil_from_days(monday)) | |
| 333 | } | |
| 334 | ||
| 335 | /// The Monday after `week` (a Monday, `YYYY-MM-DD`). | |
| 336 | pub fn next_week(week: &str) -> Option<String> { | |
| 337 | Some(civil_from_days(days_from_civil_str(week)? + 7)) | |
| 338 | } | |
| 339 | ||
| 340 | fn days_from_civil_str(date: &str) -> Option<i64> { | |
| 341 | let mut parts = date.splitn(3, '-'); | |
| 342 | let year: i64 = parts.next()?.parse().ok()?; | |
| 343 | let month: i64 = parts.next()?.parse().ok()?; | |
| 344 | let day: i64 = parts.next()?.parse().ok()?; | |
| 345 | if !(1..=12).contains(&month) || !(1..=31).contains(&day) { | |
| 346 | return None; | |
| 347 | } | |
| 348 | // Howard Hinnant's days_from_civil. | |
| 349 | let y = if month <= 2 { year - 1 } else { year }; | |
| 350 | let era = y.div_euclid(400); | |
| 351 | let yoe = y - era * 400; | |
| 352 | let mp = (month + 9) % 12; | |
| 353 | let doy = (153 * mp + 2) / 5 + day - 1; | |
| 354 | let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; | |
| 355 | Some(era * 146_097 + doe - 719_468) | |
| 356 | } | |
| 357 | ||
| 358 | fn civil_from_days(days: i64) -> String { | |
| 359 | let z = days + 719_468; | |
| 360 | let era = z.div_euclid(146_097); | |
| 361 | let doe = z - era * 146_097; | |
| 362 | let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; | |
| 363 | let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); | |
| 364 | let mp = (5 * doy + 2) / 153; | |
| 365 | let day = doy - (153 * mp + 2) / 5 + 1; | |
| 366 | let month = if mp < 10 { mp + 3 } else { mp - 9 }; | |
| 367 | let year = yoe + era * 400 + i64::from(month <= 2); | |
| 368 | format!("{year:04}-{month:02}-{day:02}") | |
| 369 | } | |
| 370 | ||
| 371 | #[cfg(test)] | |
| 372 | mod tests { | |
| 373 | use super::*; | |
| 374 | ||
| 375 | #[test] | |
| 376 | fn a_week_is_named_by_its_monday() { | |
| 377 | // 2026-10-07 is a Wednesday. | |
| 378 | assert_eq!(week_of("2026-10-07T10:00:00Z").as_deref(), Some("2026-10-05")); | |
| 379 | assert_eq!(week_of("2026-10-05T00:00:00Z").as_deref(), Some("2026-10-05")); | |
| 380 | assert_eq!(week_of("2026-10-11T23:59:59Z").as_deref(), Some("2026-10-05")); | |
| 381 | assert_eq!(week_of("2026-01-01T00:00:00Z").as_deref(), Some("2025-12-29")); | |
| 382 | assert_eq!(week_of("1970-01-01T00:00:00Z").as_deref(), Some("1969-12-29")); | |
| 383 | assert_eq!(week_of("nonsense"), None); | |
| 384 | assert_eq!(next_week("2025-12-29").as_deref(), Some("2026-01-05")); | |
| 385 | } | |
| 386 | ||
| 387 | #[test] | |
| 388 | fn the_about_is_camel_case_between_services() { | |
| 389 | let about = RepoAbout { contributors: 3, ..RepoAbout::default() }; | |
| 390 | let value = serde_json::to_value(&about).unwrap(); | |
| 391 | assert_eq!(value["topContributors"], serde_json::json!([])); | |
| 392 | assert_eq!(value["pending"], false); | |
| 393 | assert_eq!(value["securityPolicy"], serde_json::Value::Null); | |
| 394 | } | |
| 395 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.