permalink.rs

  1use std::ops::Range;
  2
  3use anyhow::{anyhow, Result};
  4use language::Point;
  5use url::Url;
  6
  7enum GitHostingProvider {
  8    Github,
  9    Gitlab,
 10    Gitee,
 11}
 12
 13impl GitHostingProvider {
 14    fn base_url(&self) -> Url {
 15        let base_url = match self {
 16            Self::Github => "https://github.com",
 17            Self::Gitlab => "https://gitlab.com",
 18            Self::Gitee => "https://gitee.com",
 19        };
 20
 21        Url::parse(&base_url).unwrap()
 22    }
 23
 24    /// Returns the fragment portion of the URL for the selected lines in
 25    /// the representation the [`GitHostingProvider`] expects.
 26    fn line_fragment(&self, selection: &Range<Point>) -> String {
 27        if selection.start.row == selection.end.row {
 28            let line = selection.start.row + 1;
 29
 30            match self {
 31                Self::Github | Self::Gitlab | Self::Gitee => format!("L{}", line),
 32            }
 33        } else {
 34            let start_line = selection.start.row + 1;
 35            let end_line = selection.end.row + 1;
 36
 37            match self {
 38                Self::Github => format!("L{}-L{}", start_line, end_line),
 39                Self::Gitlab => format!("L{}-{}", start_line, end_line),
 40                Self::Gitee => format!("L{}-{}", start_line, end_line),
 41            }
 42        }
 43    }
 44}
 45
 46pub struct BuildPermalinkParams<'a> {
 47    pub remote_url: &'a str,
 48    pub sha: &'a str,
 49    pub path: &'a str,
 50    pub selection: Option<Range<Point>>,
 51}
 52
 53pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
 54    let BuildPermalinkParams {
 55        remote_url,
 56        sha,
 57        path,
 58        selection,
 59    } = params;
 60
 61    let ParsedGitRemote {
 62        provider,
 63        owner,
 64        repo,
 65    } = parse_git_remote_url(remote_url)
 66        .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
 67
 68    let path = match provider {
 69        GitHostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"),
 70        GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"),
 71        GitHostingProvider::Gitee => format!("{owner}/{repo}/blob/{sha}/{path}"),
 72    };
 73    let line_fragment = selection.map(|selection| provider.line_fragment(&selection));
 74
 75    let mut permalink = provider.base_url().join(&path).unwrap();
 76    permalink.set_fragment(line_fragment.as_deref());
 77
 78    Ok(permalink)
 79}
 80
 81struct ParsedGitRemote<'a> {
 82    pub provider: GitHostingProvider,
 83    pub owner: &'a str,
 84    pub repo: &'a str,
 85}
 86
 87fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
 88    if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
 89        let repo_with_owner = url
 90            .trim_start_matches("git@github.com:")
 91            .trim_start_matches("https://github.com/")
 92            .trim_end_matches(".git");
 93
 94        let (owner, repo) = repo_with_owner.split_once("/")?;
 95
 96        return Some(ParsedGitRemote {
 97            provider: GitHostingProvider::Github,
 98            owner,
 99            repo,
100        });
101    }
102
103    if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") {
104        let repo_with_owner = url
105            .trim_start_matches("git@gitlab.com:")
106            .trim_start_matches("https://gitlab.com/")
107            .trim_end_matches(".git");
108
109        let (owner, repo) = repo_with_owner.split_once("/")?;
110
111        return Some(ParsedGitRemote {
112            provider: GitHostingProvider::Gitlab,
113            owner,
114            repo,
115        });
116    }
117
118    if url.starts_with("git@gitee.com:") || url.starts_with("https://gitee.com/") {
119        let repo_with_owner = url
120            .trim_start_matches("git@gitee.com:")
121            .trim_start_matches("https://gitee.com/")
122            .trim_end_matches(".git");
123
124        let (owner, repo) = repo_with_owner.split_once("/")?;
125
126        return Some(ParsedGitRemote {
127            provider: GitHostingProvider::Gitee,
128            owner,
129            repo,
130        });
131    }
132
133    None
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_build_github_permalink_from_ssh_url() {
142        let permalink = build_permalink(BuildPermalinkParams {
143            remote_url: "git@github.com:zed-industries/zed.git",
144            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
145            path: "crates/editor/src/git/permalink.rs",
146            selection: None,
147        })
148        .unwrap();
149
150        let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
151        assert_eq!(permalink.to_string(), expected_url.to_string())
152    }
153
154    #[test]
155    fn test_build_github_permalink_from_ssh_url_single_line_selection() {
156        let permalink = build_permalink(BuildPermalinkParams {
157            remote_url: "git@github.com:zed-industries/zed.git",
158            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
159            path: "crates/editor/src/git/permalink.rs",
160            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
161        })
162        .unwrap();
163
164        let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
165        assert_eq!(permalink.to_string(), expected_url.to_string())
166    }
167
168    #[test]
169    fn test_build_github_permalink_from_ssh_url_multi_line_selection() {
170        let permalink = build_permalink(BuildPermalinkParams {
171            remote_url: "git@github.com:zed-industries/zed.git",
172            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
173            path: "crates/editor/src/git/permalink.rs",
174            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
175        })
176        .unwrap();
177
178        let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
179        assert_eq!(permalink.to_string(), expected_url.to_string())
180    }
181
182    #[test]
183    fn test_build_github_permalink_from_https_url() {
184        let permalink = build_permalink(BuildPermalinkParams {
185            remote_url: "https://github.com/zed-industries/zed.git",
186            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
187            path: "crates/zed/src/main.rs",
188            selection: None,
189        })
190        .unwrap();
191
192        let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
193        assert_eq!(permalink.to_string(), expected_url.to_string())
194    }
195
196    #[test]
197    fn test_build_github_permalink_from_https_url_single_line_selection() {
198        let permalink = build_permalink(BuildPermalinkParams {
199            remote_url: "https://github.com/zed-industries/zed.git",
200            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
201            path: "crates/zed/src/main.rs",
202            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
203        })
204        .unwrap();
205
206        let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
207        assert_eq!(permalink.to_string(), expected_url.to_string())
208    }
209
210    #[test]
211    fn test_build_github_permalink_from_https_url_multi_line_selection() {
212        let permalink = build_permalink(BuildPermalinkParams {
213            remote_url: "https://github.com/zed-industries/zed.git",
214            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
215            path: "crates/zed/src/main.rs",
216            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
217        })
218        .unwrap();
219
220        let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48";
221        assert_eq!(permalink.to_string(), expected_url.to_string())
222    }
223
224    #[test]
225    fn test_build_gitlab_permalink_from_ssh_url() {
226        let permalink = build_permalink(BuildPermalinkParams {
227            remote_url: "git@gitlab.com:zed-industries/zed.git",
228            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
229            path: "crates/editor/src/git/permalink.rs",
230            selection: None,
231        })
232        .unwrap();
233
234        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
235        assert_eq!(permalink.to_string(), expected_url.to_string())
236    }
237
238    #[test]
239    fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() {
240        let permalink = build_permalink(BuildPermalinkParams {
241            remote_url: "git@gitlab.com:zed-industries/zed.git",
242            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
243            path: "crates/editor/src/git/permalink.rs",
244            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
245        })
246        .unwrap();
247
248        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
249        assert_eq!(permalink.to_string(), expected_url.to_string())
250    }
251
252    #[test]
253    fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
254        let permalink = build_permalink(BuildPermalinkParams {
255            remote_url: "git@gitlab.com:zed-industries/zed.git",
256            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
257            path: "crates/editor/src/git/permalink.rs",
258            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
259        })
260        .unwrap();
261
262        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
263        assert_eq!(permalink.to_string(), expected_url.to_string())
264    }
265
266    #[test]
267    fn test_build_gitlab_permalink_from_https_url() {
268        let permalink = build_permalink(BuildPermalinkParams {
269            remote_url: "https://gitlab.com/zed-industries/zed.git",
270            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
271            path: "crates/zed/src/main.rs",
272            selection: None,
273        })
274        .unwrap();
275
276        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
277        assert_eq!(permalink.to_string(), expected_url.to_string())
278    }
279
280    #[test]
281    fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
282        let permalink = build_permalink(BuildPermalinkParams {
283            remote_url: "https://gitlab.com/zed-industries/zed.git",
284            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
285            path: "crates/zed/src/main.rs",
286            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
287        })
288        .unwrap();
289
290        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
291        assert_eq!(permalink.to_string(), expected_url.to_string())
292    }
293
294    #[test]
295    fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
296        let permalink = build_permalink(BuildPermalinkParams {
297            remote_url: "https://gitlab.com/zed-industries/zed.git",
298            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
299            path: "crates/zed/src/main.rs",
300            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
301        })
302        .unwrap();
303
304        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
305        assert_eq!(permalink.to_string(), expected_url.to_string())
306    }
307
308    #[test]
309    fn test_build_gitee_permalink_from_ssh_url() {
310        let permalink = build_permalink(BuildPermalinkParams {
311            remote_url: "git@gitee.com:libkitten/zed.git",
312            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
313            path: "crates/editor/src/git/permalink.rs",
314            selection: None,
315        })
316        .unwrap();
317
318        let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs";
319        assert_eq!(permalink.to_string(), expected_url.to_string())
320    }
321
322    #[test]
323    fn test_build_gitee_permalink_from_ssh_url_single_line_selection() {
324        let permalink = build_permalink(BuildPermalinkParams {
325            remote_url: "git@gitee.com:libkitten/zed.git",
326            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
327            path: "crates/editor/src/git/permalink.rs",
328            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
329        })
330        .unwrap();
331
332        let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L7";
333        assert_eq!(permalink.to_string(), expected_url.to_string())
334    }
335
336    #[test]
337    fn test_build_gitee_permalink_from_ssh_url_multi_line_selection() {
338        let permalink = build_permalink(BuildPermalinkParams {
339            remote_url: "git@gitee.com:libkitten/zed.git",
340            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
341            path: "crates/editor/src/git/permalink.rs",
342            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
343        })
344        .unwrap();
345
346        let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/editor/src/git/permalink.rs#L24-48";
347        assert_eq!(permalink.to_string(), expected_url.to_string())
348    }
349
350    #[test]
351    fn test_build_gitee_permalink_from_https_url() {
352        let permalink = build_permalink(BuildPermalinkParams {
353            remote_url: "https://gitee.com/libkitten/zed.git",
354            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
355            path: "crates/zed/src/main.rs",
356            selection: None,
357        })
358        .unwrap();
359
360        let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs";
361        assert_eq!(permalink.to_string(), expected_url.to_string())
362    }
363
364    #[test]
365    fn test_build_gitee_permalink_from_https_url_single_line_selection() {
366        let permalink = build_permalink(BuildPermalinkParams {
367            remote_url: "https://gitee.com/libkitten/zed.git",
368            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
369            path: "crates/zed/src/main.rs",
370            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
371        })
372        .unwrap();
373
374        let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L7";
375        assert_eq!(permalink.to_string(), expected_url.to_string())
376    }
377
378    #[test]
379    fn test_build_gitee_permalink_from_https_url_multi_line_selection() {
380        let permalink = build_permalink(BuildPermalinkParams {
381            remote_url: "https://gitee.com/libkitten/zed.git",
382            sha: "e5fe811d7ad0fc26934edd76f891d20bdc3bb194",
383            path: "crates/zed/src/main.rs",
384            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
385        })
386        .unwrap();
387        let expected_url = "https://gitee.com/libkitten/zed/blob/e5fe811d7ad0fc26934edd76f891d20bdc3bb194/crates/zed/src/main.rs#L24-48";
388        assert_eq!(permalink.to_string(), expected_url.to_string())
389    }
390}