gitlab.rs

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