1use std::{ops::Range, sync::Arc};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use url::Url;
6use util::http::HttpClient;
7
8use crate::hosting_providers::{Bitbucket, Codeberg, Gitee, Github, Gitlab, Sourcehut};
9use crate::Oid;
10
11#[derive(Debug, PartialEq, Eq, Clone)]
12pub struct PullRequest {
13 pub number: u32,
14 pub url: Url,
15}
16
17pub struct BuildCommitPermalinkParams<'a> {
18 pub sha: &'a str,
19}
20
21pub struct BuildPermalinkParams<'a> {
22 pub sha: &'a str,
23 pub path: &'a str,
24 pub selection: Option<Range<u32>>,
25}
26
27/// A Git hosting provider.
28#[async_trait]
29pub trait GitHostingProvider {
30 /// Returns the name of the provider.
31 fn name(&self) -> String;
32
33 /// Returns the base URL of the provider.
34 fn base_url(&self) -> Url;
35
36 /// Returns a permalink to a Git commit on this hosting provider.
37 fn build_commit_permalink(
38 &self,
39 remote: &ParsedGitRemote,
40 params: BuildCommitPermalinkParams,
41 ) -> Url;
42
43 /// Returns a permalink to a file and/or selection on this hosting provider.
44 fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url;
45
46 /// Returns whether this provider supports avatars.
47 fn supports_avatars(&self) -> bool;
48
49 /// Returns a URL fragment to the given line selection.
50 fn line_fragment(&self, selection: &Range<u32>) -> String {
51 if selection.start == selection.end {
52 let line = selection.start + 1;
53
54 self.format_line_number(line)
55 } else {
56 let start_line = selection.start + 1;
57 let end_line = selection.end + 1;
58
59 self.format_line_numbers(start_line, end_line)
60 }
61 }
62
63 /// Returns a formatted line number to be placed in a permalink URL.
64 fn format_line_number(&self, line: u32) -> String;
65
66 /// Returns a formatted range of line numbers to be placed in a permalink URL.
67 fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
68
69 fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>>;
70
71 fn extract_pull_request(
72 &self,
73 _remote: &ParsedGitRemote,
74 _message: &str,
75 ) -> Option<PullRequest> {
76 None
77 }
78
79 async fn commit_author_avatar_url(
80 &self,
81 _repo_owner: &str,
82 _repo: &str,
83 _commit: Oid,
84 _http_client: Arc<dyn HttpClient>,
85 ) -> Result<Option<Url>> {
86 Ok(None)
87 }
88}
89
90#[derive(Debug)]
91pub struct ParsedGitRemote<'a> {
92 pub owner: &'a str,
93 pub repo: &'a str,
94}
95
96pub fn parse_git_remote_url(
97 url: &str,
98) -> Option<(
99 Arc<dyn GitHostingProvider + Send + Sync + 'static>,
100 ParsedGitRemote,
101)> {
102 let providers: Vec<Arc<dyn GitHostingProvider + Send + Sync + 'static>> = vec![
103 Arc::new(Github),
104 Arc::new(Gitlab),
105 Arc::new(Bitbucket),
106 Arc::new(Codeberg),
107 Arc::new(Gitee),
108 Arc::new(Sourcehut),
109 ];
110
111 providers.into_iter().find_map(|provider| {
112 provider
113 .parse_remote_url(&url)
114 .map(|parsed_remote| (provider, parsed_remote))
115 })
116}