mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-21 05:31:11 +00:00
git_hosting_providers: Add Gitiles provider (#63996)
Add Gitiles support to git hosting providers, to enable permalink / git
blame diff link features working for repositories browsed with Gitiles
(used by Gerrit).
Permalinks use the Gitiles URL format:
{base_url}/{repo}/+/{sha}/{path}#{line}
Configured via git_hosting_providers setting:
{ "provider": "gitiles", "name": "...", "base_url": "https://..." }
# Objective
- Add basic permalink + git blame linking support for another common Git
provider (Gerrit + Gitiles).
Gerrit/Gitiles is a self hosted setup used at several large
corporations, so it would be nice to have native support for it
alongside the other git hosting providers. It is relevant to many
people.
URL can vary substantially between different self hosted setups. URL can
be like https://gerrit.example.com or something more elaborate like
https://vcs.internal.example.com/plugins/gitiles.
This does not implement avatars or PR links (as the similar Chromium
provider does) since exact Gerrit setup (e.g. trailers used, avatars
used) can differ significantly between different self hosted instances.
It implements the fundamental Gitiles link part instead which is still
very useful, and is reliable; `base_url` can be set to the gitiles "Git
repositories on [instance]" [base
page](https://gerrit.googlesource.com/), and this integration will then
work.
## Solution
- Implements a new GitHostingProvider which constructs Gitiles format
permalinks based on the user config.
Implementation follows inspiration from other providers: Tangled /
Chromium (also Gitiles/Gerrit based but tied specifically to Chromium).
## Testing
- Unit tests cover the logic in this change.
- Built for Windows, performed exploratory testing (adding multiple
providers, different base_urls, trying a self hosted Gerrit instance vs
googlesource, git blame link through UI works, copy permalink via
right-click context menu in code works).
## Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments -> N/A
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines) -> N/A
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
## Showcase
#### "Copy permalink to line"
<img width="1958" height="677" alt="image"
src="https://github.com/user-attachments/assets/802a02af-db4e-4b9c-a7d5-ac26dc52cd7e"
/>
Above results in clipboard being set to:
https://gerrit.googlesource.com/gitiles/+/77d36f6ff5f50316daa82c6753af2c3217859df4/java/com/google/gitiles/GitilesUrls.java#34
#### Git blame diff view links to remote Gitiles view for the commit:
<img width="2242" height="808" alt="image"
src="https://github.com/user-attachments/assets/2792397a-83ae-4294-ae61-5a631975950d"
/>
Above example opens this link in browser:
https://gerrit.googlesource.com/gitiles/+/67c7844b7f7f228229c037cf297296874f50692c
---
`settings.json` for showcase example:
```json
"git_hosting_providers": [
{
"provider": "gitiles",
"name": "Gerrit Googlesource",
"base_url": "https://gerrit.googlesource.com"
}
],
```
---
Release Notes:
- Added `gitiles` as a Git hosting provider for Gitiles/Gerrit
deployments, enabling permalinks and navigation from git blame diff
view.
Co-authored-by: Christopher Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
parent
d9afb21688
commit
01376d4926
5 changed files with 324 additions and 3 deletions
|
|
@ -6,6 +6,7 @@ mod gerrit;
|
|||
mod gitea;
|
||||
mod gitee;
|
||||
mod github;
|
||||
mod gitiles;
|
||||
mod gitlab;
|
||||
mod sourcehut;
|
||||
mod tangled;
|
||||
|
|
@ -18,6 +19,7 @@ pub use gerrit::*;
|
|||
pub use gitea::*;
|
||||
pub use gitee::*;
|
||||
pub use github::*;
|
||||
pub use gitiles::*;
|
||||
pub use gitlab::*;
|
||||
pub use sourcehut::*;
|
||||
pub use tangled::*;
|
||||
|
|
|
|||
317
crates/git_hosting_providers/src/providers/gitiles.rs
Normal file
317
crates/git_hosting_providers/src/providers/gitiles.rs
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use git::{
|
||||
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
|
||||
RemoteUrl,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
pub struct Gitiles {
|
||||
name: String,
|
||||
base_url: Url,
|
||||
}
|
||||
|
||||
impl Gitiles {
|
||||
pub fn new(name: &str, base_url: Url) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
base_url,
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins `path` onto the base URL, preserving any existing path prefix.
|
||||
///
|
||||
/// `Url::join` cannot be used here because it drops the base path prefix
|
||||
/// (e.g. `/plugins/gitiles`) when resolving a relative reference.
|
||||
fn build_url(&self, path: &str) -> Url {
|
||||
let mut url = self.base_url();
|
||||
let base_path = url.path().trim_end_matches('/').to_string();
|
||||
url.set_path(&format!("{base_path}/{path}"));
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
impl GitHostingProvider for Gitiles {
|
||||
fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn base_url(&self) -> Url {
|
||||
self.base_url.clone()
|
||||
}
|
||||
|
||||
fn supports_avatars(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn format_line_number(&self, line: u32) -> String {
|
||||
format!("{line}")
|
||||
}
|
||||
|
||||
fn format_line_numbers(&self, start_line: u32, _end_line: u32) -> String {
|
||||
format!("{start_line}")
|
||||
}
|
||||
|
||||
fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
|
||||
let url = RemoteUrl::from_str(url).ok()?;
|
||||
|
||||
let host = url.host_str()?;
|
||||
if host != self.base_url.host_str()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path_segments = url.path_segments()?.collect::<Vec<_>>();
|
||||
let joined_path = path_segments.join("/");
|
||||
let repo = joined_path.trim_end_matches(".git");
|
||||
|
||||
Some(ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: repo.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_commit_permalink(
|
||||
&self,
|
||||
remote: &ParsedGitRemote,
|
||||
params: BuildCommitPermalinkParams,
|
||||
) -> Url {
|
||||
let BuildCommitPermalinkParams { sha } = params;
|
||||
let ParsedGitRemote { owner: _, repo } = remote;
|
||||
|
||||
self.build_url(&format!("{repo}/+/{sha}"))
|
||||
}
|
||||
|
||||
fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
|
||||
let ParsedGitRemote { owner: _, repo } = remote;
|
||||
let BuildPermalinkParams {
|
||||
sha,
|
||||
path,
|
||||
selection,
|
||||
} = params;
|
||||
|
||||
let mut permalink = self.build_url(&format!("{repo}/+/{sha}/{path}"));
|
||||
permalink.set_fragment(
|
||||
selection
|
||||
.map(|selection| self.line_fragment(&selection))
|
||||
.as_deref(),
|
||||
);
|
||||
permalink
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use git::repository::repo_path;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn android() -> Gitiles {
|
||||
Gitiles::new(
|
||||
"Android",
|
||||
Url::parse("https://android.googlesource.com").unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn gerrit() -> Gitiles {
|
||||
Gitiles::new(
|
||||
"Gerrit",
|
||||
Url::parse("https://gerrit.googlesource.com").unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn with_path_prefix() -> Gitiles {
|
||||
Gitiles::new(
|
||||
"Gitiles",
|
||||
Url::parse("https://git.example.com/plugins/gitiles").unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_https_url() {
|
||||
let parsed = android()
|
||||
.parse_remote_url("https://android.googlesource.com/platform/build/soong")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "platform/build/soong".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_ssh_url_with_port() {
|
||||
let parsed = gerrit()
|
||||
.parse_remote_url("ssh://user@gerrit.googlesource.com:29418/gitiles")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "gitiles".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_ssh_url_without_port() {
|
||||
let parsed = android()
|
||||
.parse_remote_url("ssh://user@android.googlesource.com/platform/build/soong")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "platform/build/soong".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_given_scp_style_url() {
|
||||
let parsed = gerrit()
|
||||
.parse_remote_url("git@gerrit.googlesource.com:gitiles.git")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "gitiles".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_trims_dot_git_suffix() {
|
||||
let parsed = android()
|
||||
.parse_remote_url("https://android.googlesource.com/platform/build/soong.git")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "platform/build/soong".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_with_deeply_nested_project() {
|
||||
let parsed = android()
|
||||
.parse_remote_url("https://android.googlesource.com/platform/frameworks/opt/telephony")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "platform/frameworks/opt/telephony".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_remote_url_rejects_other_host() {
|
||||
assert!(
|
||||
android()
|
||||
.parse_remote_url("https://gerrit.googlesource.com/gitiles")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitiles_permalink() {
|
||||
let permalink = gerrit().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "gitiles".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"a9409d9529e6f2da746e126dba7a8f4126f01d3e",
|
||||
&repo_path("java/com/google/gitiles/CommitData.java"),
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://gerrit.googlesource.com/gitiles/+/a9409d9529e6f2da746e126dba7a8f4126f01d3e/java/com/google/gitiles/CommitData.java";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitiles_permalink_with_single_line_selection() {
|
||||
let permalink = gerrit().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "gitiles".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"a9409d9529e6f2da746e126dba7a8f4126f01d3e",
|
||||
&repo_path("java/com/google/gitiles/CommitData.java"),
|
||||
Some(16..16),
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://gerrit.googlesource.com/gitiles/+/a9409d9529e6f2da746e126dba7a8f4126f01d3e/java/com/google/gitiles/CommitData.java#17";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitiles_permalink_with_multi_line_selection() {
|
||||
let permalink = gerrit().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "gitiles".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"a9409d9529e6f2da746e126dba7a8f4126f01d3e",
|
||||
&repo_path("java/com/google/gitiles/GitilesUrls.java"),
|
||||
Some(14..30),
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://gerrit.googlesource.com/gitiles/+/a9409d9529e6f2da746e126dba7a8f4126f01d3e/java/com/google/gitiles/GitilesUrls.java#15";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitiles_permalink_with_path_prefix() {
|
||||
let permalink = with_path_prefix().build_permalink(
|
||||
ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "project/my-repo".into(),
|
||||
},
|
||||
BuildPermalinkParams::new(
|
||||
"a9409d9529e6f2da746e126dba7a8f4126f01d3e",
|
||||
&repo_path("src/main.rs"),
|
||||
Some(9..9),
|
||||
),
|
||||
);
|
||||
|
||||
let expected_url = "https://git.example.com/plugins/gitiles/project/my-repo/+/a9409d9529e6f2da746e126dba7a8f4126f01d3e/src/main.rs#10";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitiles_commit_permalink() {
|
||||
let permalink = gerrit().build_commit_permalink(
|
||||
&ParsedGitRemote {
|
||||
owner: "".into(),
|
||||
repo: "gitiles".into(),
|
||||
},
|
||||
BuildCommitPermalinkParams {
|
||||
sha: "a9409d9529e6f2da746e126dba7a8f4126f01d3e",
|
||||
},
|
||||
);
|
||||
|
||||
let expected_url =
|
||||
"https://gerrit.googlesource.com/gitiles/+/a9409d9529e6f2da746e126dba7a8f4126f01d3e";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use settings::{
|
|||
use url::Url;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{Bitbucket, Forgejo, Gerrit, Gitea, Github, Gitlab, SourceHut, Tangled};
|
||||
use crate::{Bitbucket, Forgejo, Gerrit, Gitea, Github, Gitiles, Gitlab, SourceHut, Tangled};
|
||||
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
init_git_hosting_provider_settings(cx);
|
||||
|
|
@ -53,6 +53,7 @@ fn update_git_hosting_providers_from_settings(cx: &mut App) {
|
|||
Arc::new(SourceHut::new(&provider.name, url)) as _
|
||||
}
|
||||
GitHostingProviderKind::Tangled => Arc::new(Tangled::new(&provider.name, url)) as _,
|
||||
GitHostingProviderKind::Gitiles => Arc::new(Gitiles::new(&provider.name, url)) as _,
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1011,7 +1011,7 @@ pub enum DiagnosticSeverityContent {
|
|||
pub struct GitHostingProviderConfig {
|
||||
/// The type of the provider.
|
||||
///
|
||||
/// Must be one of `github`, `gitlab`, `bitbucket`, `gitea`, `forgejo`, `gerrit`, `sourcehut`, or `tangled`.
|
||||
/// Must be one of `github`, `gitlab`, `bitbucket`, `gitea`, `forgejo`, `gerrit`, `sourcehut`, `tangled`, or `gitiles`.
|
||||
pub provider: GitHostingProviderKind,
|
||||
|
||||
/// The base URL for the provider (e.g., "https://code.corp.big.com").
|
||||
|
|
@ -1032,6 +1032,7 @@ pub enum GitHostingProviderKind {
|
|||
Gerrit,
|
||||
SourceHut,
|
||||
Tangled,
|
||||
Gitiles,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ However, if your self-hosted Git instance URL doesn't contain identifying keywor
|
|||
}
|
||||
```
|
||||
|
||||
The `provider` field specifies which type of hosting service you're using. Supported `provider` values are `github`, `gitlab`, `bitbucket`, `gitea`, `forgejo`, `sourcehut`, and `tangled`. The `name` is optional and used as a display name for your instance, and `base_url` is the root URL of your self-hosted server.
|
||||
The `provider` field specifies which type of hosting service you're using. Supported `provider` values are `github`, `gitlab`, `bitbucket`, `gitea`, `forgejo`, `sourcehut`, `tangled`, and `gitiles`. The `name` is optional and used as a display name for your instance, and `base_url` is the root URL of your self-hosted server. For the `gitiles` provider, set `base_url` to the Gitiles browse root (e.g. `https://android.googlesource.com` or `https://git.example.com/plugins/gitiles`).
|
||||
|
||||
You can configure multiple custom providers if you work with several self-hosted instances.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue