Skip to content

Add diff support for mercurial (hg) repos #9372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion helix-term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ repository.workspace = true
homepage.workspace = true

[features]
default = ["git"]
default = ["git", "hg"]
unicode-lines = ["helix-core/unicode-lines"]
integration = []
git = ["helix-vcs/git"]
hg = ["helix-vcs/hg"]

[[bin]]
name = "hx"
Expand Down
1 change: 1 addition & 0 deletions helix-vcs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ log = "0.4"

[features]
git = ["gix"]
hg = []

[dev-dependencies]
tempfile = "3.9"
116 changes: 116 additions & 0 deletions helix-vcs/src/hg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use anyhow::{bail, Context, Result};
use arc_swap::ArcSwap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use std::process::Command;

use crate::DiffProvider;

#[cfg(test)]
mod test;

pub struct Hg;

fn exec_hg_cmd_raw(bin: &str, args: &str, root: Option<&str>) -> Result<Vec<u8>> {
let mut cmd = Command::new(bin);

cmd.env("HGPLAIN", "").env("HGRCPATH", "");

if let Some(dir) = root {
cmd.arg("--cwd").arg(dir);
}

cmd.args(args.split_whitespace());

match cmd.output() {
Ok(result) => Ok(result.stdout),
Err(e) => bail!("`hg {args}` failed: {}", e),
}
}

fn exec_hg_cmd(bin: &str, args: &str, root: Option<&str>) -> Result<String> {
match exec_hg_cmd_raw(bin, args, root) {
Ok(result) => {
Ok(String::from_utf8(result).context("Failed to parse output of `hg {args}`")?)
}
Err(e) => Err(e),
}
}

impl Hg {
fn get_repo_root(path: &Path) -> Result<PathBuf> {
if path.is_symlink() {
bail!("ignoring symlinks");
};

let workdir = if path.is_dir() {
path
} else {
path.parent().context("path has no parent")?
};

match exec_hg_cmd("rhg", "root", workdir.to_str()) {
Ok(output) => {
let root = output
.strip_suffix("\n")
.or(output.strip_suffix("\r\n"))
.unwrap_or(output.as_str());

if root.is_empty() {
bail!("did not find root")
};

let arg = format!("files {}", path.to_str().unwrap());
match exec_hg_cmd("rhg", &arg, Some(root)) {
Ok(output) => {
let tracked = output
.strip_suffix("\n")
.or(output.strip_suffix("\r\n"))
.unwrap_or(output.as_str());

if (output.len() > 0)
&& (Path::new(tracked) == path.strip_prefix(root).unwrap())
{
Ok(Path::new(&root).to_path_buf())
} else {
bail!("not a tracked file")
}
}
Err(_) => bail!("not a tracked file"),
}
}
Err(_) => bail!("not in a hg repo"),
}
}
}

impl DiffProvider for Hg {
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
debug_assert!(!file.exists() || file.is_file());
debug_assert!(file.is_absolute());

let root = Hg::get_repo_root(file).context("not a hg repo")?;

let arg = format!("cat --rev=. {}", file.to_str().unwrap());
let content =
exec_hg_cmd_raw("rhg", &arg, root.to_str()).context("could not get file content")?;

Ok(content)
}

fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
debug_assert!(!file.exists() || file.is_file());
debug_assert!(file.is_absolute());

let root = Hg::get_repo_root(file).context("not a hg repo")?;

let branch = exec_hg_cmd(
"hg",
"--config extensions.evolve= log --rev=wdir() --template={branch}",
root.to_str(),
)
.context("could not get branch name")?;
Ok(Arc::new(ArcSwap::from_pointee(branch.into_boxed_str())))
}
}
106 changes: 106 additions & 0 deletions helix-vcs/src/hg/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use std::{fs::File, io::Write, path::Path, process::Command};

use tempfile::TempDir;

use crate::{DiffProvider, Hg};

fn exec_hg_cmd(args: &str, hg_dir: &Path) {
let res = Command::new("hg")
.arg("--cwd")
.arg(hg_dir)
.args(args.split_whitespace())
.env("HGPLAIN", "")
.env("HGRCPATH", "")
.output()
.unwrap_or_else(|_| panic!("`hg {args}` failed"));
if !res.status.success() {
println!("{}", String::from_utf8_lossy(&res.stdout));
eprintln!("{}", String::from_utf8_lossy(&res.stderr));
panic!("`hg {args}` failed (see output above)")
}
}

fn create_commit(repo: &Path, add_modified: bool) {
if add_modified {
exec_hg_cmd("add", repo);
}
exec_hg_cmd("--config ui.username=foo commit -m message", repo);
}

fn empty_hg_repo() -> TempDir {
let tmp = tempfile::tempdir().expect("create temp dir for hg testing");
exec_hg_cmd("init", tmp.path());
tmp
}

#[test]
fn missing_file() {
let temp_hg = empty_hg_repo();
let file = temp_hg.path().join("file.txt");
File::create(&file).unwrap().write_all(b"foo").unwrap();

assert!(Hg.get_diff_base(&file).is_err());
}

#[test]
fn unmodified_file() {
let temp_hg = empty_hg_repo();
let file = temp_hg.path().join("file.txt");
let contents = b"foo".as_slice();
File::create(&file).unwrap().write_all(contents).unwrap();
create_commit(temp_hg.path(), true);
assert_eq!(Hg.get_diff_base(&file).unwrap(), Vec::from(contents));
}

#[test]
fn modified_file() {
let temp_hg = empty_hg_repo();
let file = temp_hg.path().join("file.txt");
let contents = b"foo".as_slice();
File::create(&file).unwrap().write_all(contents).unwrap();
create_commit(temp_hg.path(), true);
File::create(&file).unwrap().write_all(b"bar").unwrap();

assert_eq!(Hg.get_diff_base(&file).unwrap(), Vec::from(contents));
}

/// Test that `get_file_head` does not return content for a directory.
/// This is important to correctly cover cases where a directory is removed and replaced by a file.
/// If the contents of the directory object were returned a diff between a path and the directory children would be produced.
#[test]
fn directory() {
let temp_hg = empty_hg_repo();
let dir = temp_hg.path().join("file.txt");
std::fs::create_dir(&dir).expect("");
let file = dir.join("file.txt");
let contents = b"foo".as_slice();
File::create(file).unwrap().write_all(contents).unwrap();

create_commit(temp_hg.path(), true);

std::fs::remove_dir_all(&dir).unwrap();
File::create(&dir).unwrap().write_all(b"bar").unwrap();
assert!(Hg.get_diff_base(&dir).is_err());
}

/// Test that `get_file_head` does not return content for a symlink.
/// This is important to correctly cover cases where a symlink is removed and replaced by a file.
/// If the contents of the symlink object were returned a diff between a path and the actual file would be produced (bad ui).
#[cfg(any(unix, windows))]
#[test]
fn symlink() {
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(not(unix))]
use std::os::windows::fs::symlink_file as symlink;
let temp_hg = empty_hg_repo();
let file = temp_hg.path().join("file.txt");
let contents = b"foo".as_slice();
File::create(&file).unwrap().write_all(contents).unwrap();
let file_link = temp_hg.path().join("file_link.txt");
symlink("file.txt", &file_link).unwrap();

create_commit(temp_hg.path(), true);
assert!(Hg.get_diff_base(&file_link).is_err());
assert_eq!(Hg.get_diff_base(&file).unwrap(), Vec::from(contents));
}
10 changes: 9 additions & 1 deletion helix-vcs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ use std::{path::Path, sync::Arc};

#[cfg(feature = "git")]
pub use git::Git;
#[cfg(feature = "hg")]
pub use hg::Hg;

#[cfg(not(feature = "git"))]
pub use Dummy as Git;
#[cfg(not(feature = "hg"))]
pub use Dummy as Hg;

#[cfg(feature = "git")]
mod git;
#[cfg(feature = "hg")]
mod hg;

mod diff;

Expand Down Expand Up @@ -72,7 +79,8 @@ impl Default for DiffProviderRegistry {
// currently only git is supported
// TODO make this configurable when more providers are added
let git: Box<dyn DiffProvider> = Box::new(Git);
let providers = vec![git];
let hg: Box<dyn DiffProvider> = Box::new(Hg);
let providers = vec![git, hg];
DiffProviderRegistry { providers }
}
}