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}
 11
 12impl GitHostingProvider {
 13    fn base_url(&self) -> Url {
 14        let base_url = match self {
 15            Self::Github => "https://github.com",
 16            Self::Gitlab => "https://gitlab.com",
 17        };
 18
 19        Url::parse(&base_url).unwrap()
 20    }
 21
 22    /// Returns the fragment portion of the URL for the selected lines in
 23    /// the representation the [`GitHostingProvider`] expects.
 24    fn line_fragment(&self, selection: &Range<Point>) -> String {
 25        if selection.start.row == selection.end.row {
 26            let line = selection.start.row + 1;
 27
 28            match self {
 29                Self::Github | Self::Gitlab => format!("L{}", line),
 30            }
 31        } else {
 32            let start_line = selection.start.row + 1;
 33            let end_line = selection.end.row + 1;
 34
 35            match self {
 36                Self::Github => format!("L{}-L{}", start_line, end_line),
 37                Self::Gitlab => format!("L{}-{}", start_line, end_line),
 38            }
 39        }
 40    }
 41}
 42
 43pub struct BuildPermalinkParams<'a> {
 44    pub remote_url: &'a str,
 45    pub sha: &'a str,
 46    pub path: &'a str,
 47    pub selection: Option<Range<Point>>,
 48}
 49
 50pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
 51    let BuildPermalinkParams {
 52        remote_url,
 53        sha,
 54        path,
 55        selection,
 56    } = params;
 57
 58    let ParsedGitRemote {
 59        provider,
 60        owner,
 61        repo,
 62    } = parse_git_remote_url(remote_url)
 63        .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
 64
 65    let path = match provider {
 66        GitHostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"),
 67        GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"),
 68    };
 69    let line_fragment = selection.map(|selection| provider.line_fragment(&selection));
 70
 71    let mut permalink = provider.base_url().join(&path).unwrap();
 72    permalink.set_fragment(line_fragment.as_deref());
 73
 74    Ok(permalink)
 75}
 76
 77struct ParsedGitRemote<'a> {
 78    pub provider: GitHostingProvider,
 79    pub owner: &'a str,
 80    pub repo: &'a str,
 81}
 82
 83fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
 84    if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
 85        let repo_with_owner = url
 86            .trim_start_matches("git@github.com:")
 87            .trim_start_matches("https://github.com/")
 88            .trim_end_matches(".git");
 89
 90        let (owner, repo) = repo_with_owner.split_once("/")?;
 91
 92        return Some(ParsedGitRemote {
 93            provider: GitHostingProvider::Github,
 94            owner,
 95            repo,
 96        });
 97    }
 98
 99    if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") {
100        let repo_with_owner = url
101            .trim_start_matches("git@gitlab.com:")
102            .trim_start_matches("https://gitlab.com/")
103            .trim_end_matches(".git");
104
105        let (owner, repo) = repo_with_owner.split_once("/")?;
106
107        return Some(ParsedGitRemote {
108            provider: GitHostingProvider::Gitlab,
109            owner,
110            repo,
111        });
112    }
113
114    None
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_build_github_permalink_from_ssh_url() {
123        let permalink = build_permalink(BuildPermalinkParams {
124            remote_url: "git@github.com:zed-industries/zed.git",
125            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
126            path: "crates/editor/src/git/permalink.rs",
127            selection: None,
128        })
129        .unwrap();
130
131        let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
132        assert_eq!(permalink.to_string(), expected_url.to_string())
133    }
134
135    #[test]
136    fn test_build_github_permalink_from_ssh_url_single_line_selection() {
137        let permalink = build_permalink(BuildPermalinkParams {
138            remote_url: "git@github.com:zed-industries/zed.git",
139            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
140            path: "crates/editor/src/git/permalink.rs",
141            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
142        })
143        .unwrap();
144
145        let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
146        assert_eq!(permalink.to_string(), expected_url.to_string())
147    }
148
149    #[test]
150    fn test_build_github_permalink_from_ssh_url_multi_line_selection() {
151        let permalink = build_permalink(BuildPermalinkParams {
152            remote_url: "git@github.com:zed-industries/zed.git",
153            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
154            path: "crates/editor/src/git/permalink.rs",
155            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
156        })
157        .unwrap();
158
159        let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
160        assert_eq!(permalink.to_string(), expected_url.to_string())
161    }
162
163    #[test]
164    fn test_build_github_permalink_from_https_url() {
165        let permalink = build_permalink(BuildPermalinkParams {
166            remote_url: "https://github.com/zed-industries/zed.git",
167            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
168            path: "crates/zed/src/main.rs",
169            selection: None,
170        })
171        .unwrap();
172
173        let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
174        assert_eq!(permalink.to_string(), expected_url.to_string())
175    }
176
177    #[test]
178    fn test_build_github_permalink_from_https_url_single_line_selection() {
179        let permalink = build_permalink(BuildPermalinkParams {
180            remote_url: "https://github.com/zed-industries/zed.git",
181            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
182            path: "crates/zed/src/main.rs",
183            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
184        })
185        .unwrap();
186
187        let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
188        assert_eq!(permalink.to_string(), expected_url.to_string())
189    }
190
191    #[test]
192    fn test_build_github_permalink_from_https_url_multi_line_selection() {
193        let permalink = build_permalink(BuildPermalinkParams {
194            remote_url: "https://github.com/zed-industries/zed.git",
195            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
196            path: "crates/zed/src/main.rs",
197            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
198        })
199        .unwrap();
200
201        let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48";
202        assert_eq!(permalink.to_string(), expected_url.to_string())
203    }
204
205    #[test]
206    fn test_build_gitlab_permalink_from_ssh_url() {
207        let permalink = build_permalink(BuildPermalinkParams {
208            remote_url: "git@gitlab.com:zed-industries/zed.git",
209            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
210            path: "crates/editor/src/git/permalink.rs",
211            selection: None,
212        })
213        .unwrap();
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_from_ssh_url_single_line_selection() {
221        let permalink = build_permalink(BuildPermalinkParams {
222            remote_url: "git@gitlab.com:zed-industries/zed.git",
223            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
224            path: "crates/editor/src/git/permalink.rs",
225            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
226        })
227        .unwrap();
228
229        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
230        assert_eq!(permalink.to_string(), expected_url.to_string())
231    }
232
233    #[test]
234    fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
235        let permalink = build_permalink(BuildPermalinkParams {
236            remote_url: "git@gitlab.com:zed-industries/zed.git",
237            sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
238            path: "crates/editor/src/git/permalink.rs",
239            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
240        })
241        .unwrap();
242
243        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
244        assert_eq!(permalink.to_string(), expected_url.to_string())
245    }
246
247    #[test]
248    fn test_build_gitlab_permalink_from_https_url() {
249        let permalink = build_permalink(BuildPermalinkParams {
250            remote_url: "https://gitlab.com/zed-industries/zed.git",
251            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
252            path: "crates/zed/src/main.rs",
253            selection: None,
254        })
255        .unwrap();
256
257        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
258        assert_eq!(permalink.to_string(), expected_url.to_string())
259    }
260
261    #[test]
262    fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
263        let permalink = build_permalink(BuildPermalinkParams {
264            remote_url: "https://gitlab.com/zed-industries/zed.git",
265            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
266            path: "crates/zed/src/main.rs",
267            selection: Some(Point::new(6, 1)..Point::new(6, 10)),
268        })
269        .unwrap();
270
271        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
272        assert_eq!(permalink.to_string(), expected_url.to_string())
273    }
274
275    #[test]
276    fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
277        let permalink = build_permalink(BuildPermalinkParams {
278            remote_url: "https://gitlab.com/zed-industries/zed.git",
279            sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
280            path: "crates/zed/src/main.rs",
281            selection: Some(Point::new(23, 1)..Point::new(47, 10)),
282        })
283        .unwrap();
284
285        let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
286        assert_eq!(permalink.to_string(), expected_url.to_string())
287    }
288}