1use crate::{Repository, Result, VcsError};
2
3pub struct Remote;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct RemoteEntry {
19 pub name: String,
21 pub fetch_url: String,
23 pub push_url: String,
25}
26
27pub fn parse_remote_v(out: &str) -> Vec<RemoteEntry> {
41 let mut entries: Vec<RemoteEntry> = Vec::new();
42 for line in out.lines() {
43 let line = line.trim_end();
44 if line.is_empty() {
45 continue;
46 }
47 let Some((name, rest)) = line.split_once('\t') else {
48 continue;
49 };
50 let (url, kind) = match rest.rsplit_once(' ') {
51 Some((url, kind)) => (url.trim(), kind.trim()),
52 None => continue,
53 };
54 let is_push = match kind {
55 "(push)" => true,
56 "(fetch)" => false,
57 _ => continue,
58 };
59 match entries.iter_mut().find(|e| e.name == name) {
60 Some(existing) => {
61 if is_push {
62 existing.push_url = url.to_string();
63 } else {
64 existing.fetch_url = url.to_string();
65 }
66 }
67 None => entries.push(RemoteEntry {
68 name: name.to_string(),
69 fetch_url: url.to_string(),
72 push_url: url.to_string(),
73 }),
74 }
75 }
76 entries
77}
78
79impl Remote {
80 pub fn list(repo: &Repository) -> Result<Vec<RemoteEntry>> {
84 let out = repo
85 .run_git_str(["remote", "-v"])
86 .map_err(|e| VcsError::Remote(format!("remote list: {}", e)))?;
87 Ok(parse_remote_v(&out))
88 }
89
90 pub fn add(repo: &Repository, name: &str, url: &str) -> Result<()> {
94 repo.run_git(["remote", "add", name, url])
95 .map(|_| ())
96 .map_err(|e| VcsError::Remote(format!("remote add {}: {}", name, e)))
97 }
98
99 pub fn rename(repo: &Repository, old: &str, new: &str) -> Result<()> {
103 repo.run_git(["remote", "rename", old, new])
104 .map(|_| ())
105 .map_err(|e| VcsError::Remote(format!("remote rename {} -> {}: {}", old, new, e)))
106 }
107
108 pub fn remove(repo: &Repository, name: &str) -> Result<()> {
112 repo.run_git(["remote", "remove", name])
113 .map(|_| ())
114 .map_err(|e| VcsError::Remote(format!("remote remove {}: {}", name, e)))
115 }
116
117 pub fn set_url(repo: &Repository, name: &str, url: &str) -> Result<()> {
123 repo.run_git(["remote", "set-url", name, url])
124 .map(|_| ())
125 .map_err(|e| VcsError::Remote(format!("remote set-url {}: {}", name, e)))
126 }
127
128 pub fn prune(repo: &Repository, name: &str) -> Result<()> {
134 repo.run_git(["remote", "prune", name])
135 .map(|_| ())
136 .map_err(|e| VcsError::Remote(format!("remote prune {}: {}", name, e)))
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn parses_the_ordinary_two_line_per_remote_shape() {
146 let out = "origin\tgit@github.com:a/b.git (fetch)\n\
147 origin\tgit@github.com:a/b.git (push)\n\
148 upstream\thttps://example.com/c.git (fetch)\n\
149 upstream\thttps://example.com/c.git (push)\n";
150 assert_eq!(
151 parse_remote_v(out),
152 vec![
153 RemoteEntry {
154 name: "origin".into(),
155 fetch_url: "git@github.com:a/b.git".into(),
156 push_url: "git@github.com:a/b.git".into(),
157 },
158 RemoteEntry {
159 name: "upstream".into(),
160 fetch_url: "https://example.com/c.git".into(),
161 push_url: "https://example.com/c.git".into(),
162 },
163 ]
164 );
165 }
166
167 #[test]
168 fn a_separate_pushurl_shows_as_a_differing_push_url() {
169 let out = "origin\thttps://example.com/read.git (fetch)\n\
170 origin\tgit@example.com:write.git (push)\n";
171 let got = parse_remote_v(out);
172 assert_eq!(got.len(), 1);
173 assert_eq!(got[0].fetch_url, "https://example.com/read.git");
174 assert_eq!(got[0].push_url, "git@example.com:write.git");
175 }
176
177 #[test]
178 fn a_remote_printing_only_one_line_still_gets_both_urls() {
179 let out = "origin\tgit@github.com:a/b.git (fetch)\n";
182 let got = parse_remote_v(out);
183 assert_eq!(got[0].fetch_url, got[0].push_url);
184 assert!(!got[0].push_url.is_empty());
185 }
186
187 #[test]
188 fn unparseable_lines_are_skipped_and_do_not_lose_the_good_ones() {
189 let out = "garbage with no tab\n\
190 origin\tgit@github.com:a/b.git (fetch)\n\
191 weird\tsomething (mirror)\n\
192 origin\tgit@github.com:a/b.git (push)\n";
193 let got = parse_remote_v(out);
194 assert_eq!(got.len(), 1, "only origin is well-formed: {:?}", got);
195 assert_eq!(got[0].name, "origin");
196 }
197
198 #[test]
199 fn no_remotes_is_an_empty_list_not_an_error() {
200 assert!(parse_remote_v("").is_empty());
201 assert!(parse_remote_v("\n\n").is_empty());
202 }
203
204 #[test]
205 fn a_url_containing_spaces_keeps_its_whole_url() {
206 let out = "local\t/tmp/my repo/.git (fetch)\n";
209 assert_eq!(parse_remote_v(out)[0].fetch_url, "/tmp/my repo/.git");
210 }
211}