branch_names.rs

  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}