gitlab.rs

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