1use collections::HashSet;
2use rand::Rng;
3
4/// Names of historical typewriter brands, for use in auto-generated branch names.
5/// (Hyphens and parens have been dropped so that the branch names are one-word.)
6///
7/// Thanks to https://typewriterdatabase.com/alph.0.brands for the names!
8const TYPEWRITER_NAMES: &[&str] = &[
9 "abeille",
10 "acme",
11 "addo",
12 "adler",
13 "adlerette",
14 "adlerita",
15 "admiral",
16 "agamli",
17 "agar",
18 "agidel",
19 "agil",
20 "aguia",
21 "aguila",
22 "ahram",
23 "aigle",
24 "ajax",
25 "aktiv",
26 "ala",
27 "alba",
28 "albus",
29 "alexander",
30 "alexis",
31 "alfa",
32 "allen",
33 "alonso",
34 "alpina",
35 "amata",
36 "amaya",
37 "amka",
38 "anavi",
39 "anderson",
40 "andina",
41 "antares",
42 "apex",
43 "apsco",
44 "aquila",
45 "archo",
46 "ardita",
47 "argyle",
48 "aristocrat",
49 "aristokrat",
50 "arlington",
51 "armstrong",
52 "arpha",
53 "artus",
54 "astoria",
55 "atlantia",
56 "atlantic",
57 "atlas",
58 "augusta",
59 "aurora",
60 "austro",
61 "automatic",
62 "avanti",
63 "avona",
64 "azzurra",
65 "bajnok",
66 "baldwin",
67 "balkan",
68 "baltica",
69 "baltimore",
70 "barlock",
71 "barr",
72 "barrat",
73 "bartholomew",
74 "bashkiriya",
75 "bavaria",
76 "beaucourt",
77 "beko",
78 "belka",
79 "bennett",
80 "bennington",
81 "berni",
82 "bianca",
83 "bijou",
84 "bing",
85 "bisei",
86 "biser",
87 "bluebird",
88 "bolida",
89 "borgo",
90 "boston",
91 "boyce",
92 "bradford",
93 "brandenburg",
94 "brigitte",
95 "briton",
96 "brooks",
97 "brosette",
98 "buddy",
99 "burns",
100 "burroughs",
101 "byron",
102 "calanda",
103 "caligraph",
104 "cappel",
105 "cardinal",
106 "carissima",
107 "carlem",
108 "carlton",
109 "carmen",
110 "cawena",
111 "cella",
112 "celtic",
113 "century",
114 "champignon",
115 "cherryland",
116 "chevron",
117 "chicago",
118 "cicero",
119 "cifra",
120 "citizen",
121 "claudia",
122 "cleveland",
123 "clover",
124 "coffman",
125 "cole",
126 "columbia",
127 "commercial",
128 "companion",
129 "concentra",
130 "concord",
131 "concordia",
132 "conover",
133 "constanta",
134 "consul",
135 "conta",
136 "contenta",
137 "contimat",
138 "contina",
139 "continento",
140 "cornelia",
141 "coronado",
142 "cosmopolita",
143 "courier",
144 "craftamatic",
145 "crandall",
146 "crown",
147 "culema",
148 "dactyle",
149 "dankers",
150 "dart",
151 "daugherty",
152 "davis",
153 "dayton",
154 "dea",
155 "delmar",
156 "densmore",
157 "depantio",
158 "diadema",
159 "dial",
160 "diamant",
161 "diana",
162 "dictatype",
163 "diplomat",
164 "diskret",
165 "dolfus",
166 "dollar",
167 "domus",
168 "drake",
169 "draper",
170 "duplex",
171 "durabel",
172 "dynacord",
173 "eagle",
174 "eclipse",
175 "edelmann",
176 "edelweiss",
177 "edison",
178 "edita",
179 "edland",
180 "efka",
181 "eldorado",
182 "electa",
183 "electromatic",
184 "elektro",
185 "elgin",
186 "elliot",
187 "emerson",
188 "emka",
189 "emona",
190 "empire",
191 "engadine",
192 "engler",
193 "erfurt",
194 "erika",
195 "esko",
196 "essex",
197 "eureka",
198 "europa",
199 "everest",
200 "everlux",
201 "excelsior",
202 "express",
203 "fabers",
204 "facit",
205 "fairbanks",
206 "faktotum",
207 "famos",
208 "federal",
209 "felio",
210 "fidat",
211 "filius",
212 "fips",
213 "fish",
214 "fitch",
215 "fleet",
216 "florida",
217 "flott",
218 "flyer",
219 "flying",
220 "fontana",
221 "ford",
222 "forto",
223 "fortuna",
224 "fox",
225 "framo",
226 "franconia",
227 "franklin",
228 "friden",
229 "frolio",
230 "furstenberg",
231 "galesburg",
232 "galiette",
233 "gallia",
234 "garbell",
235 "gardner",
236 "geka",
237 "generation",
238 "genia",
239 "geniatus",
240 "gerda",
241 "gisela",
242 "glashutte",
243 "gloria",
244 "godrej",
245 "gossen",
246 "gourland",
247 "grandjean",
248 "granta",
249 "granville",
250 "graphic",
251 "gritzner",
252 "groma",
253 "guhl",
254 "guidonia",
255 "gundka",
256 "hacabo",
257 "haddad",
258 "halberg",
259 "halda",
260 "hall",
261 "hammond",
262 "hammonia",
263 "hanford",
264 "hansa",
265 "harmony",
266 "harris",
267 "hartford",
268 "hassia",
269 "hatch",
270 "heady",
271 "hebronia",
272 "hebros",
273 "hega",
274 "helios",
275 "helma",
276 "herald",
277 "hercules",
278 "hermes",
279 "herold",
280 "heros",
281 "hesperia",
282 "hogar",
283 "hooven",
284 "hopkins",
285 "horton",
286 "hugin",
287 "hungaria",
288 "hurtu",
289 "iberia",
290 "idea",
291 "ideal",
292 "imperia",
293 "impo",
294 "industria",
295 "industrio",
296 "ingersoll",
297 "international",
298 "invicta",
299 "irene",
300 "iris",
301 "iskra",
302 "ivitsa",
303 "ivriah",
304 "jackson",
305 "janalif",
306 "janos",
307 "jolux",
308 "juki",
309 "junior",
310 "juventa",
311 "juwel",
312 "kamkap",
313 "kamo",
314 "kanzler",
315 "kappel",
316 "karli",
317 "karstadt",
318 "keaton",
319 "kenbar",
320 "keystone",
321 "kim",
322 "klein",
323 "kneist",
324 "knoch",
325 "koh",
326 "kolibri",
327 "kolumbus",
328 "komet",
329 "kondor",
330 "koniger",
331 "konryu",
332 "kontor",
333 "kosmopolit",
334 "krypton",
335 "lambert",
336 "lasalle",
337 "lectra",
338 "leframa",
339 "lemair",
340 "lemco",
341 "liberty",
342 "libia",
343 "liga",
344 "lignose",
345 "lilliput",
346 "lindeteves",
347 "linowriter",
348 "listvitsa",
349 "ludolf",
350 "lutece",
351 "luxa",
352 "lyubava",
353 "mafra",
354 "magnavox",
355 "maher",
356 "majestic",
357 "majitouch",
358 "manhattan",
359 "mapuua",
360 "marathon",
361 "marburger",
362 "maritsa",
363 "maruzen",
364 "maskelyne",
365 "masspro",
366 "matous",
367 "mccall",
368 "mccool",
369 "mcloughlin",
370 "mead",
371 "mechno",
372 "mehano",
373 "meiselbach",
374 "melbi",
375 "melior",
376 "melotyp",
377 "mentor",
378 "mepas",
379 "mercedesia",
380 "mercurius",
381 "mercury",
382 "merkur",
383 "merritt",
384 "merz",
385 "messa",
386 "meteco",
387 "meteor",
388 "micron",
389 "mignon",
390 "mikro",
391 "minerva",
392 "mirian",
393 "mirina",
394 "mitex",
395 "molle",
396 "monac",
397 "monarch",
398 "mondiale",
399 "monica",
400 "monofix",
401 "monopol",
402 "monpti",
403 "monta",
404 "montana",
405 "montgomery",
406 "moon",
407 "morgan",
408 "morris",
409 "morse",
410 "moya",
411 "moyer",
412 "munson",
413 "musicwriter",
414 "nadex",
415 "nakajima",
416 "neckermann",
417 "neubert",
418 "neya",
419 "ninety",
420 "nisa",
421 "noiseless",
422 "noor",
423 "nora",
424 "nord",
425 "norden",
426 "norica",
427 "norma",
428 "norman",
429 "north",
430 "nototyp",
431 "nova",
432 "novalevi",
433 "odell",
434 "odhner",
435 "odo",
436 "odoma",
437 "ohio",
438 "ohtani",
439 "oliva",
440 "oliver",
441 "olivetti",
442 "olympia",
443 "omega",
444 "optima",
445 "orbis",
446 "orel",
447 "orga",
448 "oriette",
449 "orion",
450 "orn",
451 "orplid",
452 "pacior",
453 "pagina",
454 "parisienne",
455 "passat",
456 "pearl",
457 "peerless",
458 "perfect",
459 "perfecta",
460 "perkeo",
461 "perkins",
462 "perlita",
463 "pettypet",
464 "phoenix",
465 "piccola",
466 "picht",
467 "pinnock",
468 "pionier",
469 "plurotyp",
470 "plutarch",
471 "pneumatic",
472 "pocket",
473 "polyglott",
474 "polygraph",
475 "pontiac",
476 "portable",
477 "portex",
478 "pozzi",
479 "premier",
480 "presto",
481 "primavera",
482 "progress",
483 "protos",
484 "pterotype",
485 "pullman",
486 "pulsatta",
487 "quick",
488 "racer",
489 "radio",
490 "rally",
491 "rand",
492 "readers",
493 "reed",
494 "referent",
495 "reff",
496 "regent",
497 "regia",
498 "regina",
499 "rekord",
500 "reliable",
501 "reliance",
502 "remagg",
503 "rembrandt",
504 "remer",
505 "remington",
506 "remsho",
507 "remstar",
508 "remtor",
509 "reporters",
510 "resko",
511 "rex",
512 "rexpel",
513 "rheinita",
514 "rheinmetall",
515 "rival",
516 "roberts",
517 "robotron",
518 "rocher",
519 "rochester",
520 "roebuck",
521 "rofa",
522 "roland",
523 "rooy",
524 "rover",
525 "roxy",
526 "roy",
527 "royal",
528 "rundstatler",
529 "sabaudia",
530 "sabb",
531 "saleem",
532 "salter",
533 "sampo",
534 "sarafan",
535 "saturn",
536 "saxonia",
537 "schade",
538 "schapiro",
539 "schreibi",
540 "scripta",
541 "sears",
542 "secor",
543 "selectric",
544 "selekta",
545 "senator",
546 "sense",
547 "senta",
548 "serd",
549 "shilling",
550 "shimade",
551 "shimer",
552 "sholes",
553 "shuang",
554 "siegfried",
555 "siemag",
556 "silma",
557 "silver",
558 "simplex",
559 "simtype",
560 "singer",
561 "smith",
562 "soemtron",
563 "sonja",
564 "speedwriter",
565 "sphinx",
566 "starlet",
567 "stearns",
568 "steel",
569 "stella",
570 "steno",
571 "sterling",
572 "stoewer",
573 "stolzenberg",
574 "stott",
575 "strangfeld",
576 "sture",
577 "stylotyp",
578 "sun",
579 "superba",
580 "superia",
581 "supermetall",
582 "surety",
583 "swintec",
584 "swissa",
585 "talbos",
586 "talleres",
587 "tatrapoint",
588 "taurus",
589 "taylorix",
590 "tell",
591 "tempotype",
592 "tippco",
593 "titania",
594 "tops",
595 "towa",
596 "toyo",
597 "tradition",
598 "transatlantic",
599 "traveller",
600 "trebla",
601 "triumph",
602 "turia",
603 "typatune",
604 "typen",
605 "typorium",
606 "ugro",
607 "ultima",
608 "unda",
609 "underwood",
610 "unica",
611 "unitype",
612 "ursula",
613 "utax",
614 "varityper",
615 "vasanta",
616 "vendex",
617 "venus",
618 "victor",
619 "victoria",
620 "video",
621 "viking",
622 "vira",
623 "virotyp",
624 "visigraph",
625 "vittoria",
626 "volcan",
627 "vornado",
628 "voss",
629 "vultur",
630 "waltons",
631 "wanamaker",
632 "wanderer",
633 "ward",
634 "warner",
635 "waterloo",
636 "waverley",
637 "wayne",
638 "webster",
639 "wedgefield",
640 "welco",
641 "wellington",
642 "wellon",
643 "weltblick",
644 "westphalia",
645 "wiedmer",
646 "williams",
647 "wilson",
648 "winkel",
649 "winsor",
650 "wizard",
651 "woodstock",
652 "woodwards",
653 "yatran",
654 "yost",
655 "zenit",
656 "zentronik",
657 "zeta",
658 "zeya",
659];
660
661/// Picks a typewriter name that isn't already taken by an existing branch.
662///
663/// Each entry in `existing_branches` is expected to be a full branch name
664/// like `"olivetti-a3f9b2c1"`. The prefix before the last `'-'` is treated
665/// as the taken typewriter name. Branches without a `'-'` are ignored.
666///
667/// Returns `None` when every name in the pool is already taken.
668pub fn pick_typewriter_name(
669 existing_branches: &[&str],
670 rng: &mut impl Rng,
671) -> Option<&'static str> {
672 let disallowed: HashSet<&str> = existing_branches
673 .iter()
674 .filter_map(|branch| branch.rsplit_once('-').map(|(prefix, _)| prefix))
675 .collect();
676
677 let available: Vec<&'static str> = TYPEWRITER_NAMES
678 .iter()
679 .copied()
680 .filter(|name| !disallowed.contains(name))
681 .collect();
682
683 if available.is_empty() {
684 return None;
685 }
686
687 let index = rng.random_range(0..available.len());
688 Some(available[index])
689}
690
691/// Generates a branch name like `"olivetti-a3f9b2c1"` by picking a typewriter
692/// name that isn't already taken and appending an 8-character alphanumeric hash.
693///
694/// Returns `None` when every typewriter name in the pool is already taken.
695pub fn generate_branch_name(existing_branches: &[&str], rng: &mut impl Rng) -> Option<String> {
696 let typewriter_name = pick_typewriter_name(existing_branches, rng)?;
697 let hash: String = (0..8)
698 .map(|_| {
699 let idx: u8 = rng.random_range(0..36);
700 if idx < 10 {
701 (b'0' + idx) as char
702 } else {
703 (b'a' + idx - 10) as char
704 }
705 })
706 .collect();
707 Some(format!("{typewriter_name}-{hash}"))
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713 use rand::rngs::StdRng;
714
715 #[gpui::test(iterations = 10)]
716 fn test_pick_typewriter_name_with_no_disallowed(mut rng: StdRng) {
717 let name = pick_typewriter_name(&[], &mut rng);
718 assert!(name.is_some());
719 assert!(TYPEWRITER_NAMES.contains(&name.unwrap()));
720 }
721
722 #[gpui::test(iterations = 10)]
723 fn test_pick_typewriter_name_excludes_taken_names(mut rng: StdRng) {
724 let branch_names = &["olivetti-abc12345", "selectric-def67890"];
725 let name = pick_typewriter_name(branch_names, &mut rng).unwrap();
726 assert_ne!(name, "olivetti");
727 assert_ne!(name, "selectric");
728 }
729
730 #[gpui::test]
731 fn test_pick_typewriter_name_all_taken(mut rng: StdRng) {
732 let branch_names: Vec<String> = TYPEWRITER_NAMES
733 .iter()
734 .map(|name| format!("{name}-00000000"))
735 .collect();
736 let branch_name_refs: Vec<&str> = branch_names.iter().map(|s| s.as_str()).collect();
737 let name = pick_typewriter_name(&branch_name_refs, &mut rng);
738 assert!(name.is_none());
739 }
740
741 #[gpui::test(iterations = 10)]
742 fn test_pick_typewriter_name_ignores_branches_without_hyphen(mut rng: StdRng) {
743 let branch_names = &["main", "develop", "feature"];
744 let name = pick_typewriter_name(branch_names, &mut rng);
745 assert!(name.is_some());
746 assert!(TYPEWRITER_NAMES.contains(&name.unwrap()));
747 }
748
749 #[gpui::test(iterations = 10)]
750 fn test_generate_branch_name_format(mut rng: StdRng) {
751 let branch_name = generate_branch_name(&[], &mut rng).unwrap();
752 let (prefix, suffix) = branch_name.rsplit_once('-').unwrap();
753 assert!(TYPEWRITER_NAMES.contains(&prefix));
754 assert_eq!(suffix.len(), 8);
755 assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
756 }
757
758 #[gpui::test]
759 fn test_generate_branch_name_returns_none_when_exhausted(mut rng: StdRng) {
760 let branch_names: Vec<String> = TYPEWRITER_NAMES
761 .iter()
762 .map(|name| format!("{name}-00000000"))
763 .collect();
764 let branch_name_refs: Vec<&str> = branch_names.iter().map(|s| s.as_str()).collect();
765 let result = generate_branch_name(&branch_name_refs, &mut rng);
766 assert!(result.is_none());
767 }
768
769 #[gpui::test(iterations = 100)]
770 fn test_generate_branch_name_never_reuses_taken_prefix(mut rng: StdRng) {
771 let existing = &["olivetti-123abc", "selectric-def456"];
772 let branch_name = generate_branch_name(existing, &mut rng).unwrap();
773 let (prefix, _) = branch_name.rsplit_once('-').unwrap();
774 assert_ne!(prefix, "olivetti");
775 assert_ne!(prefix, "selectric");
776 }
777
778 #[gpui::test(iterations = 100)]
779 fn test_generate_branch_name_avoids_multiple_taken_prefixes(mut rng: StdRng) {
780 let existing = &[
781 "olivetti-aaa11111",
782 "selectric-bbb22222",
783 "corona-ccc33333",
784 "remington-ddd44444",
785 "underwood-eee55555",
786 ];
787 let taken_prefixes: HashSet<&str> = existing
788 .iter()
789 .filter_map(|b| b.rsplit_once('-').map(|(prefix, _)| prefix))
790 .collect();
791 let branch_name = generate_branch_name(existing, &mut rng).unwrap();
792 let (prefix, _) = branch_name.rsplit_once('-').unwrap();
793 assert!(
794 !taken_prefixes.contains(prefix),
795 "generated prefix {prefix:?} collides with an existing branch"
796 );
797 }
798
799 #[gpui::test(iterations = 100)]
800 fn test_generate_branch_name_with_varied_hash_suffixes(mut rng: StdRng) {
801 let existing = &[
802 "olivetti-aaaaaaaa",
803 "olivetti-bbbbbbbb",
804 "olivetti-cccccccc",
805 ];
806 let branch_name = generate_branch_name(existing, &mut rng).unwrap();
807 let (prefix, _) = branch_name.rsplit_once('-').unwrap();
808 assert_ne!(
809 prefix, "olivetti",
810 "should avoid olivetti regardless of how many variants exist"
811 );
812 }
813
814 #[test]
815 fn test_typewriter_names_are_valid() {
816 let mut seen = HashSet::default();
817 for &name in TYPEWRITER_NAMES {
818 assert!(
819 seen.insert(name),
820 "duplicate entry in TYPEWRITER_NAMES: {name:?}"
821 );
822 }
823
824 for window in TYPEWRITER_NAMES.windows(2) {
825 assert!(
826 window[0] <= window[1],
827 "TYPEWRITER_NAMES is not sorted: {0:?} should come after {1:?}",
828 window[1],
829 window[0],
830 );
831 }
832
833 for &name in TYPEWRITER_NAMES {
834 assert!(
835 !name.contains('-'),
836 "TYPEWRITER_NAMES entry contains a hyphen: {name:?}"
837 );
838 }
839
840 for &name in TYPEWRITER_NAMES {
841 assert!(
842 name.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()),
843 "TYPEWRITER_NAMES entry is not lowercase: {name:?}"
844 );
845 }
846 }
847}