branch_names.rs

  1use collections::HashSet;
  2use rand::Rng;
  3
  4const ADJECTIVES: &[&str] = &[
  5    "able", "agate", "agile", "alpine", "amber", "ample", "aqua", "arctic", "arid", "astral",
  6    "autumn", "avid", "azure", "balmy", "birch", "bold", "boreal", "brave", "breezy", "brief",
  7    "bright", "brisk", "broad", "bronze", "calm", "cerith", "civil", "clean", "clear", "clever",
  8    "cobalt", "cool", "copper", "coral", "cozy", "crisp", "cubic", "cyan", "deft", "dense", "dewy",
  9    "direct", "dusky", "dusty", "eager", "early", "earnest", "elder", "elfin", "equal", "even",
 10    "exact", "faint", "fair", "fast", "fawn", "ferny", "fiery", "fine", "firm", "fleet", "floral",
 11    "focal", "fond", "frank", "fresh", "frosty", "full", "gentle", "gilded", "glacial", "glad",
 12    "glossy", "golden", "grand", "green", "gusty", "hale", "happy", "hardy", "hazel", "hearty",
 13    "hilly", "humble", "hushed", "icy", "ideal", "inner", "iron", "ivory", "jade", "jovial",
 14    "keen", "kind", "lapis", "leafy", "level", "light", "lilac", "limber", "lively", "local",
 15    "lofty", "lucid", "lunar", "major", "maple", "mellow", "merry", "mild", "milky", "misty",
 16    "modal", "modest", "mossy", "muted", "native", "naval", "neat", "nimble", "noble", "north",
 17    "novel", "oaken", "ochre", "olive", "onyx", "opal", "open", "optic", "outer", "owed", "ozone",
 18    "pale", "pastel", "pearl", "pecan", "peppy", "pilot", "placid", "plain", "plum", "plush",
 19    "poised", "polar", "polished", "poplar", "prime", "proof", "proud", "pure", "quartz", "quick",
 20    "quiet", "rapid", "raspy", "ready", "regal", "rooted", "rosy", "round", "royal", "ruby",
 21    "ruddy", "russet", "rustic", "sage", "salty", "sandy", "satin", "scenic", "sedge", "serene",
 22    "sharp", "sheer", "silky", "silver", "sleek", "smart", "smooth", "snowy", "solar", "solid",
 23    "south", "spry", "stark", "steady", "steel", "steep", "still", "stoic", "stony", "stout",
 24    "sturdy", "suede", "sunny", "supple", "sure", "swift", "tall", "tawny", "teal", "terse",
 25    "thick", "tidal", "tidy", "timber", "topaz", "total", "trim", "tropic", "true", "tulip",
 26    "upper", "urban", "valid", "vast", "velvet", "verde", "vivid", "vocal", "warm", "waxen",
 27    "west", "whole", "wide", "wild", "wise", "witty", "woven", "young", "zealous", "zephyr",
 28    "zesty", "zinc",
 29];
 30
 31const NOUNS: &[&str] = &[
 32    "anchor", "anvil", "arbor", "arch", "arrow", "atlas", "badge", "badger", "basin", "bay",
 33    "beacon", "beam", "bell", "birch", "blade", "bloom", "bluff", "bolt", "bower", "breeze",
 34    "bridge", "brook", "bunting", "cabin", "cairn", "canyon", "cape", "cedar", "chasm", "cliff",
 35    "cloud", "clover", "coast", "cobble", "colt", "comet", "condor", "coral", "cove", "crane",
 36    "crater", "creek", "crest", "curlew", "cypress", "dale", "dawn", "delta", "den", "dove",
 37    "drake", "drift", "drum", "dune", "dusk", "eagle", "echo", "egret", "elk", "elm", "ember",
 38    "falcon", "fawn", "fern", "ferry", "field", "finch", "fjord", "flame", "flint", "flower",
 39    "forge", "fossil", "fox", "frost", "gale", "garnet", "gate", "gazelle", "geyser", "glade",
 40    "glen", "gorge", "granite", "grove", "gull", "harbor", "hare", "haven", "hawk", "hazel",
 41    "heath", "hedge", "heron", "hill", "hollow", "horizon", "ibis", "inlet", "isle", "ivy",
 42    "jackal", "jasper", "juniper", "kestrel", "kinglet", "knoll", "lagoon", "lake", "lantern",
 43    "larch", "lark", "laurel", "lava", "leaf", "ledge", "lily", "linden", "lodge", "loft", "lotus",
 44    "lynx", "mantle", "maple", "marble", "marsh", "marten", "meadow", "merlin", "mesa", "mill",
 45    "mint", "moon", "moose", "moss", "newt", "north", "nutmeg", "oak", "oasis", "obsidian",
 46    "orbit", "orchid", "oriole", "osprey", "otter", "owl", "palm", "panther", "pass", "path",
 47    "peak", "pebble", "pelican", "peony", "perch", "pier", "pine", "plover", "plume", "pond",
 48    "poppy", "prairie", "prism", "puma", "quail", "quarry", "quartz", "rain", "rampart", "range",
 49    "raven", "ravine", "reed", "reef", "ridge", "river", "robin", "rowan", "sage", "salmon",
 50    "sequoia", "shore", "shrike", "sigma", "sky", "slate", "slope", "snow", "spark", "sparrow",
 51    "spider", "spruce", "stag", "star", "stone", "stork", "storm", "stream", "summit", "swift",
 52    "sycamore", "tern", "terrace", "thistle", "thorn", "thrush", "tide", "timber", "torch",
 53    "tower", "trail", "trout", "tulip", "tundra", "vale", "valley", "veranda", "viper", "vista",
 54    "vole", "walrus", "warbler", "willow", "wolf", "wren", "yew", "zenith",
 55];
 56
 57/// Generates a branch name in `"adjective-noun"` format (e.g. `"swift-falcon"`).
 58///
 59/// Tries up to 100 random combinations, skipping any name that already appears
 60/// in `existing_branches`. Returns `None` if no unused name is found.
 61pub fn generate_branch_name(existing_branches: &[&str], rng: &mut impl Rng) -> Option<String> {
 62    let existing: HashSet<&str> = existing_branches.iter().copied().collect();
 63
 64    for _ in 0..100 {
 65        let adjective = ADJECTIVES[rng.random_range(0..ADJECTIVES.len())];
 66        let noun = NOUNS[rng.random_range(0..NOUNS.len())];
 67        let name = format!("{adjective}-{noun}");
 68
 69        if !existing.contains(name.as_str()) {
 70            return Some(name);
 71        }
 72    }
 73
 74    None
 75}
 76
 77#[cfg(test)]
 78mod tests {
 79    use super::*;
 80    use rand::rngs::StdRng;
 81
 82    #[gpui::test(iterations = 10)]
 83    fn test_generate_branch_name_format(mut rng: StdRng) {
 84        let name = generate_branch_name(&[], &mut rng).unwrap();
 85        let (adjective, noun) = name.split_once('-').expect("name should contain a hyphen");
 86        assert!(
 87            ADJECTIVES.contains(&adjective),
 88            "{adjective:?} is not in ADJECTIVES"
 89        );
 90        assert!(NOUNS.contains(&noun), "{noun:?} is not in NOUNS");
 91    }
 92
 93    #[gpui::test(iterations = 100)]
 94    fn test_generate_branch_name_avoids_existing(mut rng: StdRng) {
 95        let existing = &["swift-falcon", "calm-river", "bold-cedar"];
 96        let name = generate_branch_name(existing, &mut rng).unwrap();
 97        for &branch in existing {
 98            assert_ne!(
 99                name, branch,
100                "generated name should not match an existing branch"
101            );
102        }
103    }
104
105    #[gpui::test]
106    fn test_generate_branch_name_returns_none_when_stuck(mut rng: StdRng) {
107        let all_names: Vec<String> = ADJECTIVES
108            .iter()
109            .flat_map(|adj| NOUNS.iter().map(move |noun| format!("{adj}-{noun}")))
110            .collect();
111        let refs: Vec<&str> = all_names.iter().map(|s| s.as_str()).collect();
112        let result = generate_branch_name(&refs, &mut rng);
113        assert!(result.is_none());
114    }
115
116    #[test]
117    fn test_adjectives_are_valid() {
118        let mut seen = HashSet::default();
119        for &word in ADJECTIVES {
120            assert!(seen.insert(word), "duplicate entry in ADJECTIVES: {word:?}");
121        }
122
123        for window in ADJECTIVES.windows(2) {
124            assert!(
125                window[0] < window[1],
126                "ADJECTIVES is not sorted: {0:?} should come before {1:?}",
127                window[0],
128                window[1],
129            );
130        }
131
132        for &word in ADJECTIVES {
133            assert!(
134                !word.contains('-'),
135                "ADJECTIVES entry contains a hyphen: {word:?}"
136            );
137            assert!(
138                word.chars().all(|c| c.is_lowercase()),
139                "ADJECTIVES entry is not all lowercase: {word:?}"
140            );
141        }
142    }
143
144    #[test]
145    fn test_nouns_are_valid() {
146        let mut seen = HashSet::default();
147        for &word in NOUNS {
148            assert!(seen.insert(word), "duplicate entry in NOUNS: {word:?}");
149        }
150
151        for window in NOUNS.windows(2) {
152            assert!(
153                window[0] < window[1],
154                "NOUNS is not sorted: {0:?} should come before {1:?}",
155                window[0],
156                window[1],
157            );
158        }
159
160        for &word in NOUNS {
161            assert!(
162                !word.contains('-'),
163                "NOUNS entry contains a hyphen: {word:?}"
164            );
165            assert!(
166                word.chars().all(|c| c.is_lowercase()),
167                "NOUNS entry is not all lowercase: {word:?}"
168            );
169        }
170    }
171}