gitlab.rs

  1use std::str::FromStr;
  2
  3use anyhow::{anyhow, bail, Result};
  4use url::Url;
  5use util::maybe;
  6
  7use git::{
  8    BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
  9    RemoteUrl,
 10};
 11
 12#[derive(Debug)]
 13pub struct Gitlab {
 14    name: String,
 15    base_url: Url,
 16}
 17
 18impl Gitlab {
 19    pub fn new() -> Self {
 20        Self {
 21            name: "GitLab".to_string(),
 22            base_url: Url::parse("https://gitlab.com").unwrap(),
 23        }
 24    }
 25
 26    pub fn from_remote_url(remote_url: &str) -> Result<Self> {
 27        let host = maybe!({
 28            if let Some(remote_url) = remote_url.strip_prefix("git@") {
 29                if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') {
 30                    return Some(host.to_string());
 31                }
 32            }
 33
 34            Url::parse(&remote_url)
 35                .ok()
 36                .and_then(|remote_url| remote_url.host_str().map(|host| host.to_string()))
 37        })
 38        .ok_or_else(|| anyhow!("URL has no host"))?;
 39
 40        if !host.contains("gitlab") {
 41            bail!("not a GitLab URL");
 42        }
 43
 44        Ok(Self {
 45            name: "GitLab Self-Hosted".to_string(),
 46            base_url: Url::parse(&format!("https://{}", host))?,
 47        })
 48    }
 49}
 50
 51impl GitHostingProvider for Gitlab {
 52    fn name(&self) -> String {
 53        self.name.clone()
 54    }
 55
 56    fn base_url(&self) -> Url {
 57        self.base_url.clone()
 58    }
 59
 60    fn supports_avatars(&self) -> bool {
 61        false
 62    }
 63
 64    fn format_line_number(&self, line: u32) -> String {
 65        format!("L{line}")
 66    }
 67
 68    fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
 69        format!("L{start_line}-{end_line}")
 70    }
 71
 72    fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
 73        let url = RemoteUrl::from_str(url).ok()?;
 74
 75        let host = url.host_str()?;
 76        if host != self.base_url.host_str()? {
 77            return None;
 78        }
 79
 80        let mut path_segments = url.path_segments()?;
 81        let owner = path_segments.next()?;
 82        let repo = path_segments.next()?.trim_end_matches(".git");
 83
 84        Some(ParsedGitRemote {
 85            owner: owner.into(),
 86            repo: repo.into(),
 87        })
 88    }
 89
 90    fn build_commit_permalink(
 91        &self,
 92        remote: &ParsedGitRemote,
 93        params: BuildCommitPermalinkParams,
 94    ) -> Url {
 95        let BuildCommitPermalinkParams { sha } = params;
 96        let ParsedGitRemote { owner, repo } = remote;
 97
 98        self.base_url()
 99            .join(&format!("{owner}/{repo}/-/commit/{sha}"))
100            .unwrap()
101    }
102
103    fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
104        let ParsedGitRemote { owner, repo } = remote;
105        let BuildPermalinkParams {
106            sha,
107            path,
108            selection,
109        } = params;
110
111        let mut permalink = self
112            .base_url()
113            .join(&format!("{owner}/{repo}/-/blob/{sha}/{path}"))
114            .unwrap();
115        if path.ends_with(".md") {
116            permalink.set_query(Some("plain=1"));
117        }
118        permalink.set_fragment(
119            selection
120                .map(|selection| self.line_fragment(&selection))
121                .as_deref(),
122        );
123        permalink
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use pretty_assertions::assert_eq;
130
131    use super::*;
132
133    #[test]
134    fn test_build_gitlab_permalink_from_ssh_url() {
135        let remote = ParsedGitRemote {
136            owner: "zed-industries".into(),
137            repo: "zed".into(),
138        };
139        let permalink = Gitlab::new().build_permalink(
140            remote,
141            BuildPermalinkParams {
142                sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
143                path: "crates/editor/src/git/permalink.rs",
144                selection: None,
145            },
146        );
147
148        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
149        assert_eq!(permalink.to_string(), expected_url.to_string())
150    }
151
152    #[test]
153    fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() {
154        let remote = ParsedGitRemote {
155            owner: "zed-industries".into(),
156            repo: "zed".into(),
157        };
158        let permalink = Gitlab::new().build_permalink(
159            remote,
160            BuildPermalinkParams {
161                sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
162                path: "crates/editor/src/git/permalink.rs",
163                selection: Some(6..6),
164            },
165        );
166
167        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
168        assert_eq!(permalink.to_string(), expected_url.to_string())
169    }
170
171    #[test]
172    fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
173        let remote = ParsedGitRemote {
174            owner: "zed-industries".into(),
175            repo: "zed".into(),
176        };
177        let permalink = Gitlab::new().build_permalink(
178            remote,
179            BuildPermalinkParams {
180                sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
181                path: "crates/editor/src/git/permalink.rs",
182                selection: Some(23..47),
183            },
184        );
185
186        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
187        assert_eq!(permalink.to_string(), expected_url.to_string())
188    }
189
190    #[test]
191    fn test_build_gitlab_permalink_from_https_url() {
192        let remote = ParsedGitRemote {
193            owner: "zed-industries".into(),
194            repo: "zed".into(),
195        };
196        let permalink = Gitlab::new().build_permalink(
197            remote,
198            BuildPermalinkParams {
199                sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
200                path: "crates/zed/src/main.rs",
201                selection: None,
202            },
203        );
204
205        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
206        assert_eq!(permalink.to_string(), expected_url.to_string())
207    }
208
209    #[test]
210    fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
211        let remote = ParsedGitRemote {
212            owner: "zed-industries".into(),
213            repo: "zed".into(),
214        };
215        let permalink = Gitlab::new().build_permalink(
216            remote,
217            BuildPermalinkParams {
218                sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
219                path: "crates/zed/src/main.rs",
220                selection: Some(6..6),
221            },
222        );
223
224        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
225        assert_eq!(permalink.to_string(), expected_url.to_string())
226    }
227
228    #[test]
229    fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
230        let remote = ParsedGitRemote {
231            owner: "zed-industries".into(),
232            repo: "zed".into(),
233        };
234        let permalink = Gitlab::new().build_permalink(
235            remote,
236            BuildPermalinkParams {
237                sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
238                path: "crates/zed/src/main.rs",
239                selection: Some(23..47),
240            },
241        );
242
243        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
244        assert_eq!(permalink.to_string(), expected_url.to_string())
245    }
246
247    #[test]
248    fn test_build_gitlab_self_hosted_permalink_from_ssh_url() {
249        let remote = ParsedGitRemote {
250            owner: "zed-industries".into(),
251            repo: "zed".into(),
252        };
253        let gitlab =
254            Gitlab::from_remote_url("git@gitlab.some-enterprise.com:zed-industries/zed.git")
255                .unwrap();
256        let permalink = gitlab.build_permalink(
257            remote,
258            BuildPermalinkParams {
259                sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
260                path: "crates/editor/src/git/permalink.rs",
261                selection: None,
262            },
263        );
264
265        let expected_url = "https://gitlab.some-enterprise.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
266        assert_eq!(permalink.to_string(), expected_url.to_string())
267    }
268
269    #[test]
270    fn test_build_gitlab_self_hosted_permalink_from_https_url() {
271        let remote = ParsedGitRemote {
272            owner: "zed-industries".into(),
273            repo: "zed".into(),
274        };
275        let gitlab =
276            Gitlab::from_remote_url("https://gitlab-instance.big-co.com/zed-industries/zed.git")
277                .unwrap();
278        let permalink = gitlab.build_permalink(
279            remote,
280            BuildPermalinkParams {
281                sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
282                path: "crates/zed/src/main.rs",
283                selection: None,
284            },
285        );
286
287        let expected_url = "https://gitlab-instance.big-co.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
288        assert_eq!(permalink.to_string(), expected_url.to_string())
289    }
290}