Fix permalink-to-line when Git repo root and worktree dir don't coincide (#22003)

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
This commit is contained in:
Cole Miller 2024-12-19 14:23:50 -05:00 committed by GitHub
parent 1e2fa3b022
commit 2094d50514
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 91 additions and 8 deletions

View file

@ -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

View file

@ -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)
}