Cargo.lock 🔗
@@ -9592,6 +9592,7 @@ dependencies = [
"tempfile",
"terminal",
"text",
+ "toml 0.8.19",
"unindent",
"url",
"util",
Cole Miller created
Closes #21505. This should work if the git dir is an ancestor of the
worktree dir or vice versa.
Release Notes:
- Fixed GitHub permalink-to-line actions when worktree dir and Git dir
aren't the same
Cargo.lock | 1
crates/project/Cargo.toml | 1
crates/project/src/buffer_store.rs | 96 +++++++++++++++++++++++++++++--
crates/worktree/src/worktree.rs | 1
4 files changed, 91 insertions(+), 8 deletions(-)
@@ -9592,6 +9592,7 @@ dependencies = [
"tempfile",
"terminal",
"text",
+ "toml 0.8.19",
"unindent",
"url",
"util",
@@ -68,6 +68,7 @@ snippet.workspace = true
snippet_provider.workspace = true
terminal.workspace = true
text.workspace = true
+toml.workspace = true
util.workspace = true
url.workspace = true
which.workspace = true
@@ -5,12 +5,12 @@ use crate::{
ProjectItem as _, ProjectPath,
};
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
-use anyhow::{anyhow, Context as _, Result};
+use anyhow::{anyhow, bail, Context as _, Result};
use client::Client;
use collections::{hash_map, HashMap, HashSet};
use fs::Fs;
use futures::{channel::oneshot, future::Shared, Future, FutureExt as _, StreamExt};
-use git::{blame::Blame, diff::BufferDiff};
+use git::{blame::Blame, diff::BufferDiff, repository::RepoPath};
use gpui::{
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Subscription,
Task, WeakModel,
@@ -24,8 +24,16 @@ use language::{
Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation,
};
use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
+use serde::Deserialize;
use smol::channel::Receiver;
-use std::{io, ops::Range, path::Path, str::FromStr as _, sync::Arc, time::Instant};
+use std::{
+ io,
+ ops::Range,
+ path::{Path, PathBuf},
+ str::FromStr as _,
+ sync::Arc,
+ time::Instant,
+};
use text::{BufferId, LineEnding, Rope};
use util::{debug_panic, maybe, ResultExt as _, TryFutureExt};
use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId};
@@ -1213,11 +1221,36 @@ impl BufferStore {
match file.worktree.read(cx) {
Worktree::Local(worktree) => {
- let Some(repo) = worktree.local_git_repo(file.path()) else {
- return Task::ready(Err(anyhow!("no repository for buffer found")));
+ let worktree_path = worktree.abs_path().clone();
+ let Some((repo_entry, repo)) =
+ worktree.repository_for_path(file.path()).and_then(|entry| {
+ let repo = worktree.get_local_repo(&entry)?.repo().clone();
+ Some((entry, repo))
+ })
+ else {
+ // If we're not in a Git repo, check whether this is a Rust source
+ // file in the Cargo registry (presumably opened with go-to-definition
+ // from a normal Rust file). If so, we can put together a permalink
+ // using crate metadata.
+ if !buffer
+ .language()
+ .is_some_and(|lang| lang.name() == "Rust".into())
+ {
+ return Task::ready(Err(anyhow!("no permalink available")));
+ }
+ let file_path = worktree_path.join(file.path());
+ return cx.spawn(|cx| async move {
+ let provider_registry =
+ cx.update(GitHostingProviderRegistry::default_global)?;
+ get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
+ .map_err(|_| anyhow!("no permalink available"))
+ });
};
- let path = file.path().clone();
+ let path = match repo_entry.relativize(worktree, file.path()) {
+ Ok(RepoPath(path)) => path,
+ Err(e) => return Task::ready(Err(e)),
+ };
cx.spawn(|cx| async move {
const REMOTE_NAME: &str = "origin";
@@ -1238,7 +1271,7 @@ impl BufferStore {
let path = path
.to_str()
- .context("failed to convert buffer path to string")?;
+ .ok_or_else(|| anyhow!("failed to convert path to string"))?;
Ok(provider.build_permalink(
remote,
@@ -2432,3 +2465,52 @@ fn deserialize_blame_buffer_response(
remote_url: response.remote_url,
})
}
+
+fn get_permalink_in_rust_registry_src(
+ provider_registry: Arc<GitHostingProviderRegistry>,
+ path: PathBuf,
+ selection: Range<u32>,
+) -> Result<url::Url> {
+ #[derive(Deserialize)]
+ struct CargoVcsGit {
+ sha1: String,
+ }
+
+ #[derive(Deserialize)]
+ struct CargoVcsInfo {
+ git: CargoVcsGit,
+ path_in_vcs: String,
+ }
+
+ #[derive(Deserialize)]
+ struct CargoPackage {
+ repository: String,
+ }
+
+ #[derive(Deserialize)]
+ struct CargoToml {
+ package: CargoPackage,
+ }
+
+ let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| {
+ let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?;
+ Some((dir, json))
+ }) else {
+ bail!("No .cargo_vcs_info.json found in parent directories")
+ };
+ let cargo_vcs_info = serde_json::from_str::<CargoVcsInfo>(&cargo_vcs_info_json)?;
+ let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?;
+ let manifest = toml::from_str::<CargoToml>(&cargo_toml)?;
+ let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository)
+ .ok_or_else(|| anyhow!("Failed to parse package.repository field of manifest"))?;
+ let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap());
+ let permalink = provider.build_permalink(
+ remote,
+ BuildPermalinkParams {
+ sha: &cargo_vcs_info.git.sha1,
+ path: &path.to_string_lossy(),
+ selection: Some(selection),
+ },
+ );
+ Ok(permalink)
+}
@@ -2396,7 +2396,6 @@ impl Snapshot {
.map(|(path, entry)| (&path.0, entry))
}
- /// Get the repository whose work directory contains the given path.
pub fn repository_for_work_directory(&self, path: &Path) -> Option<RepositoryEntry> {
self.repository_entries
.get(&RepositoryWorkDirectory(path.into()))