Stabilize fallback (#1471)
This commit is contained in:
parent
bb5b962c3d
commit
10ad32430b
@ -92,20 +92,20 @@ impl Subcommand {
|
||||
arguments: &[String],
|
||||
overrides: &BTreeMap<String, String>,
|
||||
) -> Result<(), Error<'src>> {
|
||||
if config.unstable
|
||||
&& matches!(
|
||||
config.search_config,
|
||||
SearchConfig::FromInvocationDirectory | SearchConfig::FromSearchDirectory { .. }
|
||||
)
|
||||
{
|
||||
let mut path = match &config.search_config {
|
||||
if matches!(
|
||||
config.search_config,
|
||||
SearchConfig::FromInvocationDirectory | SearchConfig::FromSearchDirectory { .. }
|
||||
) {
|
||||
let starting_path = match &config.search_config {
|
||||
SearchConfig::FromInvocationDirectory => config.invocation_directory.clone(),
|
||||
SearchConfig::FromSearchDirectory { search_directory } => std::env::current_dir()
|
||||
.unwrap()
|
||||
.join(search_directory.clone()),
|
||||
SearchConfig::FromSearchDirectory { search_directory } => {
|
||||
std::env::current_dir().unwrap().join(search_directory)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let mut path = starting_path.clone();
|
||||
|
||||
let mut unknown_recipes_errors = None;
|
||||
|
||||
loop {
|
||||
@ -116,11 +116,10 @@ impl Subcommand {
|
||||
},
|
||||
Err(err) => return Err(err.into()),
|
||||
Ok(search) => {
|
||||
if config.verbosity.loud() && path != config.invocation_directory {
|
||||
if config.verbosity.loud() && path != starting_path {
|
||||
eprintln!(
|
||||
"Trying {}",
|
||||
config
|
||||
.invocation_directory
|
||||
starting_path
|
||||
.strip_prefix(path)
|
||||
.unwrap()
|
||||
.components()
|
||||
|
@ -31,7 +31,7 @@ fn allow_duplicate_recipes_with_args() {
|
||||
set allow-duplicate-recipes
|
||||
",
|
||||
)
|
||||
.args(&["b", "one", "two"])
|
||||
.args(["b", "one", "two"])
|
||||
.stdout("bar one two\n")
|
||||
.stderr("echo bar one two\n")
|
||||
.run();
|
||||
|
@ -3,7 +3,7 @@ use super::*;
|
||||
#[test]
|
||||
fn print_changelog() {
|
||||
Test::new()
|
||||
.args(&["--changelog"])
|
||||
.args(["--changelog"])
|
||||
.stdout(fs::read_to_string("CHANGELOG.md").unwrap())
|
||||
.run();
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ fn invoke_error_function() {
|
||||
.stderr_regex("error: Chooser `/ -cu fzf` invocation failed: .*")
|
||||
.status(EXIT_FAILURE)
|
||||
.shell(false)
|
||||
.args(&["--shell", "/", "--choose"])
|
||||
.args(["--shell", "/", "--choose"])
|
||||
.run();
|
||||
}
|
||||
|
||||
|
@ -2,26 +2,14 @@ use super::*;
|
||||
|
||||
#[test]
|
||||
fn dotenv() {
|
||||
let tmp = temptree! {
|
||||
".env": "KEY=ROOT",
|
||||
sub: {
|
||||
".env": "KEY=SUB",
|
||||
justfile: "default:\n\techo KEY=${KEY:-unset}",
|
||||
},
|
||||
};
|
||||
|
||||
let binary = executable_path("just");
|
||||
|
||||
let output = Command::new(binary)
|
||||
.current_dir(tmp.path())
|
||||
.arg("sub/default")
|
||||
.output()
|
||||
.expect("just invocation failed");
|
||||
|
||||
assert_eq!(output.status.code().unwrap(), 0);
|
||||
|
||||
let stdout = str::from_utf8(&output.stdout).unwrap();
|
||||
assert_eq!(stdout, "KEY=unset\n");
|
||||
Test::new()
|
||||
.write(".env", "KEY=ROOT")
|
||||
.write("sub/.env", "KEY=SUB")
|
||||
.write("sub/justfile", "default:\n\techo KEY=${KEY:-unset}")
|
||||
.args(["sub/default"])
|
||||
.stdout("KEY=unset\n")
|
||||
.stderr("echo KEY=${KEY:-unset}\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
test! {
|
||||
@ -83,7 +71,7 @@ fn path_not_found() {
|
||||
echo $NAME
|
||||
",
|
||||
)
|
||||
.args(&["--dotenv-path", ".env.prod"])
|
||||
.args(["--dotenv-path", ".env.prod"])
|
||||
.stderr(if cfg!(windows) {
|
||||
"error: Failed to load environment file: The system cannot find the file specified. (os \
|
||||
error 2)\n"
|
||||
@ -108,7 +96,7 @@ fn path_resolves() {
|
||||
".env": "NAME=bar"
|
||||
}
|
||||
})
|
||||
.args(&["--dotenv-path", "subdir/.env"])
|
||||
.args(["--dotenv-path", "subdir/.env"])
|
||||
.stdout("bar\n")
|
||||
.status(EXIT_SUCCESS)
|
||||
.run();
|
||||
@ -126,7 +114,7 @@ fn filename_resolves() {
|
||||
.tree(tree! {
|
||||
".env.special": "NAME=bar"
|
||||
})
|
||||
.args(&["--dotenv-filename", ".env.special"])
|
||||
.args(["--dotenv-filename", ".env.special"])
|
||||
.stdout("bar\n")
|
||||
.status(EXIT_SUCCESS)
|
||||
.run();
|
||||
@ -146,7 +134,7 @@ fn filename_flag_overwrites_no_load() {
|
||||
.tree(tree! {
|
||||
".env.special": "NAME=bar"
|
||||
})
|
||||
.args(&["--dotenv-filename", ".env.special"])
|
||||
.args(["--dotenv-filename", ".env.special"])
|
||||
.stdout("bar\n")
|
||||
.status(EXIT_SUCCESS)
|
||||
.run();
|
||||
@ -168,7 +156,7 @@ fn path_flag_overwrites_no_load() {
|
||||
".env": "NAME=bar"
|
||||
}
|
||||
})
|
||||
.args(&["--dotenv-path", "subdir/.env"])
|
||||
.args(["--dotenv-path", "subdir/.env"])
|
||||
.stdout("bar\n")
|
||||
.status(EXIT_SUCCESS)
|
||||
.run();
|
||||
|
@ -40,7 +40,7 @@ test! {
|
||||
fn argument_count_mismatch() {
|
||||
Test::new()
|
||||
.justfile("foo a b:")
|
||||
.args(&["foo"])
|
||||
.args(["foo"])
|
||||
.stderr(
|
||||
"
|
||||
error: Recipe `foo` got 0 arguments but takes 2
|
||||
|
@ -1,5 +1,45 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fallback_from_subdir_bugfix() {
|
||||
Test::new()
|
||||
.write(
|
||||
"sub/justfile",
|
||||
unindent(
|
||||
"
|
||||
set fallback
|
||||
|
||||
@default:
|
||||
echo foo
|
||||
",
|
||||
),
|
||||
)
|
||||
.args(["sub/default"])
|
||||
.stdout("foo\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_from_subdir_message() {
|
||||
Test::new()
|
||||
.justfile("bar:\n echo bar")
|
||||
.write(
|
||||
"sub/justfile",
|
||||
unindent(
|
||||
"
|
||||
set fallback
|
||||
|
||||
@foo:
|
||||
echo foo
|
||||
",
|
||||
),
|
||||
)
|
||||
.args(["sub/bar"])
|
||||
.stderr(path("Trying ../justfile\necho bar\n"))
|
||||
.stdout("bar\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_recipe_in_parent_if_not_found_in_current() {
|
||||
Test::new()
|
||||
@ -19,7 +59,7 @@ fn runs_recipe_in_parent_if_not_found_in_current() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "foo"])
|
||||
.args(["foo"])
|
||||
.current_dir("bar")
|
||||
.stderr(format!(
|
||||
"
|
||||
@ -51,7 +91,7 @@ fn setting_accepts_value() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "foo"])
|
||||
.args(["foo"])
|
||||
.current_dir("bar")
|
||||
.stderr(format!(
|
||||
"
|
||||
@ -78,7 +118,7 @@ fn print_error_from_parent_if_recipe_not_found_in_current() {
|
||||
}
|
||||
})
|
||||
.justfile("foo:\n echo {{bar}}")
|
||||
.args(&["--unstable", "foo"])
|
||||
.args(["foo"])
|
||||
.current_dir("bar")
|
||||
.stderr(format!(
|
||||
"
|
||||
@ -95,31 +135,6 @@ fn print_error_from_parent_if_recipe_not_found_in_current() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_unstable() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
bar: {
|
||||
justfile: "
|
||||
baz:
|
||||
echo subdir
|
||||
"
|
||||
}
|
||||
})
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile does not contain recipe `foo`.\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn requires_setting() {
|
||||
Test::new()
|
||||
.tree(tree! {
|
||||
@ -136,7 +151,7 @@ fn requires_setting() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "foo"])
|
||||
.args(["foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile does not contain recipe `foo`.\n")
|
||||
@ -162,7 +177,7 @@ fn works_with_provided_search_directory() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "./foo"])
|
||||
.args(["./foo"])
|
||||
.stdout("root\n")
|
||||
.stderr(format!(
|
||||
"
|
||||
@ -192,7 +207,7 @@ fn doesnt_work_with_justfile() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "--justfile", "justfile", "foo"])
|
||||
.args(["--justfile", "justfile", "foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile does not contain recipe `foo`.\n")
|
||||
@ -216,14 +231,7 @@ fn doesnt_work_with_justfile_and_working_directory() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&[
|
||||
"--unstable",
|
||||
"--justfile",
|
||||
"justfile",
|
||||
"--working-directory",
|
||||
".",
|
||||
"foo",
|
||||
])
|
||||
.args(["--justfile", "justfile", "--working-directory", ".", "foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile does not contain recipe `foo`.\n")
|
||||
@ -249,7 +257,7 @@ fn prints_correct_error_message_when_recipe_not_found() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "foo"])
|
||||
.args(["foo"])
|
||||
.current_dir("bar")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr(format!(
|
||||
@ -289,7 +297,7 @@ fn multiple_levels_of_fallback_work() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "baz"])
|
||||
.args(["baz"])
|
||||
.current_dir("a/b")
|
||||
.stdout("root\n")
|
||||
.stderr(format!(
|
||||
@ -328,7 +336,7 @@ fn stop_fallback_when_fallback_is_false() {
|
||||
echo root
|
||||
",
|
||||
)
|
||||
.args(&["--unstable", "baz"])
|
||||
.args(["baz"])
|
||||
.current_dir("a/b")
|
||||
.stderr(format!(
|
||||
"
|
@ -105,7 +105,7 @@ fn write_error() {
|
||||
|
||||
let test = Test::with_tempdir(tempdir)
|
||||
.no_justfile()
|
||||
.args(&["--fmt", "--unstable"])
|
||||
.args(["--fmt", "--unstable"])
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr_regex(if cfg!(windows) {
|
||||
r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n"
|
||||
@ -1055,7 +1055,7 @@ test! {
|
||||
fn exported_parameter() {
|
||||
Test::new()
|
||||
.justfile("foo +$f:")
|
||||
.args(&["--dump"])
|
||||
.args(["--dump"])
|
||||
.stdout("foo +$f:\n")
|
||||
.run();
|
||||
}
|
||||
|
@ -414,7 +414,7 @@ test! {
|
||||
fn assert_eval_eq(expression: &str, result: &str) {
|
||||
Test::new()
|
||||
.justfile(format!("x := {}", expression))
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout(result)
|
||||
.unindent_stdout(false)
|
||||
.run();
|
||||
@ -478,7 +478,7 @@ fn join() {
|
||||
fn join_argument_count_error() {
|
||||
Test::new()
|
||||
.justfile("x := join('a')")
|
||||
.args(&["--evaluate"])
|
||||
.args(["--evaluate"])
|
||||
.stderr(
|
||||
"
|
||||
error: Function `join` called with 1 argument but takes 2 or more
|
||||
@ -498,7 +498,7 @@ fn test_path_exists_filepath_exist() {
|
||||
testfile: ""
|
||||
})
|
||||
.justfile("x := path_exists('testfile')")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("true")
|
||||
.run();
|
||||
}
|
||||
@ -507,7 +507,7 @@ fn test_path_exists_filepath_exist() {
|
||||
fn test_path_exists_filepath_doesnt_exist() {
|
||||
Test::new()
|
||||
.justfile("x := path_exists('testfile')")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("false")
|
||||
.run();
|
||||
}
|
||||
@ -516,7 +516,7 @@ fn test_path_exists_filepath_doesnt_exist() {
|
||||
fn error_errors_with_message() {
|
||||
Test::new()
|
||||
.justfile("x := error ('Thing Not Supported')")
|
||||
.args(&["--evaluate"])
|
||||
.args(["--evaluate"])
|
||||
.status(1)
|
||||
.stderr("error: Call to function `error` failed: Thing Not Supported\n |\n1 | x := error ('Thing Not Supported')\n | ^^^^^\n")
|
||||
.run();
|
||||
@ -526,7 +526,7 @@ fn error_errors_with_message() {
|
||||
fn test_absolute_path_resolves() {
|
||||
let test_object = Test::new()
|
||||
.justfile("path := absolute_path('./test_file')")
|
||||
.args(&["--evaluate", "path"]);
|
||||
.args(["--evaluate", "path"]);
|
||||
|
||||
let mut tempdir = test_object.tempdir.path().to_owned();
|
||||
|
||||
@ -546,7 +546,7 @@ fn test_absolute_path_resolves() {
|
||||
fn test_absolute_path_resolves_parent() {
|
||||
let test_object = Test::new()
|
||||
.justfile("path := absolute_path('../test_file')")
|
||||
.args(&["--evaluate", "path"]);
|
||||
.args(["--evaluate", "path"]);
|
||||
|
||||
let mut tempdir = test_object.tempdir.path().to_owned();
|
||||
|
||||
@ -580,7 +580,7 @@ fn path_exists_subdir() {
|
||||
})
|
||||
.justfile("x := path_exists('foo')")
|
||||
.current_dir("bar")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("true")
|
||||
.run();
|
||||
}
|
||||
@ -589,7 +589,7 @@ fn path_exists_subdir() {
|
||||
fn uuid() {
|
||||
Test::new()
|
||||
.justfile("x := uuid()")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout_regex("........-....-....-....-............")
|
||||
.run();
|
||||
}
|
||||
@ -598,7 +598,7 @@ fn uuid() {
|
||||
fn sha256() {
|
||||
Test::new()
|
||||
.justfile("x := sha256('5943ee37-0000-1000-8000-010203040506')")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("2330d7f5eb94a820b54fed59a8eced236f80b633a504289c030b6a65aef58871")
|
||||
.run();
|
||||
}
|
||||
@ -613,7 +613,7 @@ fn sha256_file() {
|
||||
}
|
||||
})
|
||||
.current_dir("sub")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("177b3d79aaafb53a7a4d7aaba99a82f27c73370e8cb0295571aade1e4fea1cd2")
|
||||
.run();
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ fn write_error() {
|
||||
|
||||
test
|
||||
.no_justfile()
|
||||
.args(&["--init"])
|
||||
.args(["--init"])
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr_regex(if cfg!(windows) {
|
||||
r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n"
|
||||
|
@ -3,7 +3,7 @@ use super::*;
|
||||
fn test(justfile: &str, value: Value) {
|
||||
Test::new()
|
||||
.justfile(justfile)
|
||||
.args(&["--dump", "--dump-format", "json", "--unstable"])
|
||||
.args(["--dump", "--dump-format", "json", "--unstable"])
|
||||
.stdout(format!("{}\n", serde_json::to_string(&value).unwrap()))
|
||||
.run();
|
||||
}
|
||||
@ -709,7 +709,7 @@ fn quiet() {
|
||||
fn requires_unstable() {
|
||||
Test::new()
|
||||
.justfile("foo:")
|
||||
.args(&["--dump", "--dump-format", "json"])
|
||||
.args(["--dump", "--dump-format", "json"])
|
||||
.stderr("error: The JSON dump format is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n")
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
|
10
tests/lib.rs
10
tests/lib.rs
@ -47,7 +47,7 @@ mod error_messages;
|
||||
mod evaluate;
|
||||
mod examples;
|
||||
mod export;
|
||||
mod fall_back_to_parent;
|
||||
mod fallback;
|
||||
mod fmt;
|
||||
mod functions;
|
||||
mod ignore_comments;
|
||||
@ -83,3 +83,11 @@ mod undefined_variables;
|
||||
#[cfg(target_family = "windows")]
|
||||
mod windows_shell;
|
||||
mod working_directory;
|
||||
|
||||
fn path(s: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
s.replace('/', "\\")
|
||||
} else {
|
||||
s.into()
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ fn private_attribute_for_recipe() {
|
||||
foo:
|
||||
",
|
||||
)
|
||||
.args(&["--list"])
|
||||
.args(["--list"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
@ -29,7 +29,7 @@ fn private_attribute_for_alias() {
|
||||
foo:
|
||||
",
|
||||
)
|
||||
.args(&["--list"])
|
||||
.args(["--list"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
|
@ -8,7 +8,7 @@ fn single_quotes_are_prepended_and_appended() {
|
||||
x := quote('abc')
|
||||
",
|
||||
)
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("'abc'")
|
||||
.run();
|
||||
}
|
||||
@ -21,7 +21,7 @@ fn quotes_are_escaped() {
|
||||
x := quote("'")
|
||||
"#,
|
||||
)
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout(r"''\'''")
|
||||
.run();
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ fn dont_run_duplicate_recipes() {
|
||||
# foo
|
||||
",
|
||||
)
|
||||
.args(&["foo", "foo"])
|
||||
.args(["foo", "foo"])
|
||||
.stderr(
|
||||
"
|
||||
# foo
|
||||
|
@ -4,7 +4,7 @@ use super::*;
|
||||
fn once() {
|
||||
Test::new()
|
||||
.justfile("x := 'a' / 'b'")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("a/b")
|
||||
.run();
|
||||
}
|
||||
@ -13,7 +13,7 @@ fn once() {
|
||||
fn twice() {
|
||||
Test::new()
|
||||
.justfile("x := 'a' / 'b' / 'c'")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("a/b/c")
|
||||
.run();
|
||||
}
|
||||
@ -22,7 +22,7 @@ fn twice() {
|
||||
fn no_lhs_once() {
|
||||
Test::new()
|
||||
.justfile("x := / 'a'")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("/a")
|
||||
.run();
|
||||
}
|
||||
@ -31,12 +31,12 @@ fn no_lhs_once() {
|
||||
fn no_lhs_twice() {
|
||||
Test::new()
|
||||
.justfile("x := / 'a' / 'b'")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("/a/b")
|
||||
.run();
|
||||
Test::new()
|
||||
.justfile("x := // 'a'")
|
||||
.args(&["--evaluate", "x"])
|
||||
.args(["--evaluate", "x"])
|
||||
.stdout("//a")
|
||||
.run();
|
||||
}
|
||||
|
@ -78,8 +78,8 @@ impl Test {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn args(mut self, args: &[&str]) -> Self {
|
||||
for arg in args {
|
||||
pub(crate) fn args<'a>(mut self, args: impl AsRef<[&'a str]>) -> Self {
|
||||
for arg in args.as_ref() {
|
||||
self = self.arg(arg);
|
||||
}
|
||||
self
|
||||
@ -154,6 +154,13 @@ impl Test {
|
||||
self.unindent_stdout = unindent_stdout;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn write(self, path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> Self {
|
||||
let path = self.tempdir.path().join(path);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, content).unwrap();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Test {
|
||||
|
Loading…
Reference in New Issue
Block a user