Rebuilt from the exact zed-industries/zed @ 8c74db00c4053d6c0a9037863d7bd048658da4ca tree (previous orphan was based on a stale/wrong fetch). Same 3-file patch: - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
389 lines
12 KiB
Rust
389 lines
12 KiB
Rust
use std::str::FromStr;
|
|
use std::sync::LazyLock;
|
|
|
|
use async_trait::async_trait;
|
|
use regex::Regex;
|
|
use url::Url;
|
|
|
|
use git::{
|
|
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
|
|
PullRequest, RemoteUrl,
|
|
};
|
|
|
|
fn pull_request_regex() -> &'static Regex {
|
|
static PULL_REQUEST_REGEX: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"^Merged PR (\d+):").unwrap());
|
|
&PULL_REQUEST_REGEX
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Azure;
|
|
|
|
impl Azure {
|
|
fn parse_dev_azure_com_url(&self, url: &RemoteUrl) -> Option<ParsedGitRemote> {
|
|
let host = url.host_str()?;
|
|
|
|
if host == "ssh.dev.azure.com" {
|
|
// SSH format: git@ssh.dev.azure.com:v3/{organization}/{project}/{repo}
|
|
let mut path_segments = url.path_segments()?;
|
|
let _v3 = path_segments.next()?;
|
|
let organization = path_segments.next()?;
|
|
let project = path_segments.next()?;
|
|
let repo = path_segments.next()?.trim_end_matches(".git");
|
|
|
|
return Some(ParsedGitRemote {
|
|
owner: format!("{organization}/{project}").into(),
|
|
repo: repo.into(),
|
|
});
|
|
}
|
|
|
|
if host != "dev.azure.com" {
|
|
return None;
|
|
}
|
|
|
|
// HTTPS format: https://dev.azure.com/{organization}/{project}/_git/{repo}
|
|
// or: https://{organization}@dev.azure.com/{organization}/{project}/_git/{repo}
|
|
let mut path_segments = url.path_segments()?;
|
|
let organization = path_segments.next()?;
|
|
let project = path_segments.next()?;
|
|
let _git = path_segments.next()?;
|
|
let repo = path_segments.next()?.trim_end_matches(".git");
|
|
|
|
Some(ParsedGitRemote {
|
|
owner: format!("{organization}/{project}").into(),
|
|
repo: repo.into(),
|
|
})
|
|
}
|
|
|
|
fn parse_visualstudio_com_url(&self, url: &RemoteUrl) -> Option<ParsedGitRemote> {
|
|
let host = url.host_str()?;
|
|
|
|
if !host.ends_with(".visualstudio.com") {
|
|
return None;
|
|
}
|
|
|
|
let organization = host.strip_suffix(".visualstudio.com")?;
|
|
|
|
// HTTPS format: https://{organization}.visualstudio.com/{project}/_git/{repo}
|
|
// or with DefaultCollection: https://{organization}.visualstudio.com/DefaultCollection/{project}/_git/{repo}
|
|
let mut path_segments = url.path_segments()?.peekable();
|
|
|
|
let first_segment = path_segments.next()?;
|
|
let project = if first_segment == "DefaultCollection" {
|
|
path_segments.next()?
|
|
} else {
|
|
first_segment
|
|
};
|
|
|
|
let _git = path_segments.next()?;
|
|
let repo = path_segments.next()?.trim_end_matches(".git");
|
|
|
|
Some(ParsedGitRemote {
|
|
owner: format!("{organization}/{project}").into(),
|
|
repo: repo.into(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl GitHostingProvider for Azure {
|
|
fn name(&self) -> String {
|
|
"Azure DevOps".to_string()
|
|
}
|
|
|
|
fn base_url(&self) -> Url {
|
|
Url::parse("https://dev.azure.com").unwrap()
|
|
}
|
|
|
|
fn supports_avatars(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn format_line_number(&self, line: u32) -> String {
|
|
format!("line={line}&lineEnd={line}&lineStartColumn=1&lineEndColumn=1")
|
|
}
|
|
|
|
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String {
|
|
format!("line={start_line}&lineEnd={end_line}&lineStartColumn=1&lineEndColumn=1")
|
|
}
|
|
|
|
fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote> {
|
|
let url = RemoteUrl::from_str(url).ok()?;
|
|
|
|
self.parse_dev_azure_com_url(&url)
|
|
.or_else(|| self.parse_visualstudio_com_url(&url))
|
|
}
|
|
|
|
fn build_commit_permalink(
|
|
&self,
|
|
remote: &ParsedGitRemote,
|
|
params: BuildCommitPermalinkParams,
|
|
) -> Url {
|
|
let BuildCommitPermalinkParams { sha } = params;
|
|
let ParsedGitRemote { owner, repo } = remote;
|
|
|
|
let mut url = self
|
|
.base_url()
|
|
.join(&format!("{owner}/_git/{repo}/commit/{sha}"))
|
|
.expect("failed to build commit permalink");
|
|
url.set_query(None);
|
|
url
|
|
}
|
|
|
|
fn build_permalink(&self, remote: ParsedGitRemote, params: BuildPermalinkParams) -> Url {
|
|
let ParsedGitRemote { owner, repo } = remote;
|
|
let BuildPermalinkParams {
|
|
sha,
|
|
path,
|
|
selection,
|
|
} = params;
|
|
|
|
let mut permalink = self
|
|
.base_url()
|
|
.join(&format!("{owner}/_git/{repo}"))
|
|
.expect("failed to build permalink");
|
|
|
|
let mut query = format!("path=/{path}&version=GC{sha}");
|
|
if let Some(selection) = selection {
|
|
query.push('&');
|
|
query.push_str(&self.line_fragment(&selection));
|
|
}
|
|
permalink.set_query(Some(&query));
|
|
|
|
permalink
|
|
}
|
|
|
|
fn build_create_pull_request_url(
|
|
&self,
|
|
remote: &ParsedGitRemote,
|
|
source_branch: &str,
|
|
) -> Option<Url> {
|
|
let ParsedGitRemote { owner, repo } = remote;
|
|
let encoded_source = urlencoding::encode(source_branch);
|
|
|
|
let mut url = self
|
|
.base_url()
|
|
.join(&format!("{owner}/_git/{repo}/pullrequestcreate"))
|
|
.ok()?;
|
|
url.set_query(Some(&format!("sourceRef={encoded_source}")));
|
|
Some(url)
|
|
}
|
|
|
|
fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option<PullRequest> {
|
|
let first_line = message.lines().next()?;
|
|
let capture = pull_request_regex().captures(first_line)?;
|
|
let number = capture.get(1)?.as_str().parse::<u32>().ok()?;
|
|
|
|
let ParsedGitRemote { owner, repo } = remote;
|
|
let url = self
|
|
.base_url()
|
|
.join(&format!("{owner}/_git/{repo}/pullrequest/{number}"))
|
|
.ok()?;
|
|
|
|
Some(PullRequest { number, url })
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use git::repository::repo_path;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_parse_remote_url_https_dev_azure() {
|
|
let parsed_remote = Azure
|
|
.parse_remote_url("https://dev.azure.com/myorg/myproject/_git/myrepo")
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
parsed_remote,
|
|
ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_remote_url_https_dev_azure_with_username() {
|
|
let parsed_remote = Azure
|
|
.parse_remote_url("https://myorg@dev.azure.com/myorg/myproject/_git/myrepo")
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
parsed_remote,
|
|
ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_remote_url_ssh_dev_azure() {
|
|
let parsed_remote = Azure
|
|
.parse_remote_url("git@ssh.dev.azure.com:v3/myorg/myproject/myrepo")
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
parsed_remote,
|
|
ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_remote_url_visualstudio_com() {
|
|
let parsed_remote = Azure
|
|
.parse_remote_url("https://myorg.visualstudio.com/myproject/_git/myrepo")
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
parsed_remote,
|
|
ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_remote_url_visualstudio_com_with_default_collection() {
|
|
let parsed_remote = Azure
|
|
.parse_remote_url(
|
|
"https://myorg.visualstudio.com/DefaultCollection/myproject/_git/myrepo",
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
parsed_remote,
|
|
ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_remote_url_returns_none_for_github() {
|
|
let result = Azure.parse_remote_url("https://github.com/owner/repo.git");
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_azure_permalink() {
|
|
let permalink = Azure.build_permalink(
|
|
ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
},
|
|
BuildPermalinkParams::new("abc123def456", &repo_path("src/main.rs"), None),
|
|
);
|
|
|
|
let expected_url = "https://dev.azure.com/myorg/myproject/_git/myrepo?path=/src/main.rs&version=GCabc123def456";
|
|
assert_eq!(permalink.to_string(), expected_url.to_string())
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_azure_permalink_with_single_line_selection() {
|
|
let permalink = Azure.build_permalink(
|
|
ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
},
|
|
BuildPermalinkParams::new("abc123def456", &repo_path("src/main.rs"), Some(6..6)),
|
|
);
|
|
|
|
let expected_url = "https://dev.azure.com/myorg/myproject/_git/myrepo?path=/src/main.rs&version=GCabc123def456&line=7&lineEnd=7&lineStartColumn=1&lineEndColumn=1";
|
|
assert_eq!(permalink.to_string(), expected_url.to_string())
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_azure_permalink_with_multi_line_selection() {
|
|
let permalink = Azure.build_permalink(
|
|
ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
},
|
|
BuildPermalinkParams::new("abc123def456", &repo_path("src/main.rs"), Some(23..47)),
|
|
);
|
|
|
|
let expected_url = "https://dev.azure.com/myorg/myproject/_git/myrepo?path=/src/main.rs&version=GCabc123def456&line=24&lineEnd=48&lineStartColumn=1&lineEndColumn=1";
|
|
assert_eq!(permalink.to_string(), expected_url.to_string())
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_azure_commit_permalink() {
|
|
let permalink = Azure.build_commit_permalink(
|
|
&ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
},
|
|
BuildCommitPermalinkParams {
|
|
sha: "abc123def456",
|
|
},
|
|
);
|
|
|
|
let expected_url = "https://dev.azure.com/myorg/myproject/_git/myrepo/commit/abc123def456";
|
|
assert_eq!(permalink.to_string(), expected_url.to_string())
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_azure_create_pr_url() {
|
|
let remote = ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
};
|
|
|
|
let url = Azure
|
|
.build_create_pull_request_url(&remote, "feature/my-branch")
|
|
.expect("url should be constructed");
|
|
|
|
assert_eq!(
|
|
url.as_str(),
|
|
"https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequestcreate?sourceRef=feature%2Fmy-branch"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_azure_extract_pull_request() {
|
|
use indoc::indoc;
|
|
|
|
let remote = ParsedGitRemote {
|
|
owner: "myorg/myproject".into(),
|
|
repo: "myrepo".into(),
|
|
};
|
|
|
|
let message = "This does not contain a pull request";
|
|
assert!(Azure.extract_pull_request(&remote, message).is_none());
|
|
|
|
let message = indoc! {r#"
|
|
Merged PR 123: Add new feature
|
|
|
|
This PR adds a new feature to the application.
|
|
"#};
|
|
|
|
let pull_request = Azure.extract_pull_request(&remote, message).unwrap();
|
|
assert_eq!(pull_request.number, 123);
|
|
assert_eq!(
|
|
pull_request.url.as_str(),
|
|
"https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequest/123"
|
|
);
|
|
|
|
let message = "Merged PR 456: Fix bug in authentication";
|
|
let pull_request = Azure.extract_pull_request(&remote, message).unwrap();
|
|
assert_eq!(pull_request.number, 456);
|
|
assert_eq!(
|
|
pull_request.url.as_str(),
|
|
"https://dev.azure.com/myorg/myproject/_git/myrepo/pullrequest/456"
|
|
);
|
|
|
|
let message = "This mentions PR 789 but not at the start";
|
|
assert!(Azure.extract_pull_request(&remote, message).is_none());
|
|
}
|
|
}
|