mirror of
https://github.com/facebook/sapling.git
synced 2025-01-07 14:10:42 +03:00
add an eden remove
command
Summary: Add a `remove` command to the Eden CLI. This behaves like `eden unmount --destroy`, but calling this "remove" is hopefully a more intuitive UI. If stdin is a TTY this command also prompts the user for confirmation before removing the checkout. I plan to deprecate the `eden unmount --destroy` command in a subsequent diff. Reviewed By: wez Differential Revision: D8086823 fbshipit-source-id: 562cf0f998eea416b80589b188eee255a10b9699
This commit is contained in:
parent
f8b2f2830c
commit
b32e6b3e57
@ -12,12 +12,14 @@ import errno
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import readline # noqa: F401 Importing readline improves the behavior of input()
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
from typing import Any, List, Optional, Set, Tuple
|
||||
|
||||
import eden.thrift
|
||||
from eden.thrift import EdenNotRunningError
|
||||
from facebook.eden import EdenService
|
||||
|
||||
@ -71,7 +73,7 @@ def do_version(args: argparse.Namespace) -> int:
|
||||
print("Running: %s" % rv)
|
||||
if rv.startswith("-") or rv.endswith("-"):
|
||||
print("(Dev version of eden seems to be running)")
|
||||
except eden.thrift.client.EdenNotRunningError:
|
||||
except EdenNotRunningError:
|
||||
print("Running: Unknown (edenfs does not appear to be running)")
|
||||
return 0
|
||||
|
||||
@ -445,6 +447,66 @@ class MountCmd(Subcmd):
|
||||
return 0
|
||||
|
||||
|
||||
@subcmd("remove", "Remove an eden checkout", aliases=["rm"])
|
||||
class RemoveCmd(Subcmd):
|
||||
|
||||
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"-y",
|
||||
"--yes",
|
||||
"--no-prompt",
|
||||
dest="prompt",
|
||||
default=True,
|
||||
action="store_false",
|
||||
help="Do not prompt for confirmation before removing the checkouts.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"paths", nargs="+", metavar="path", help="The Eden checkout(s) to remove"
|
||||
)
|
||||
|
||||
def run(self, args: argparse.Namespace) -> int:
|
||||
config = create_config(args)
|
||||
|
||||
# First translate the list of paths into mount point names
|
||||
mounts = []
|
||||
for path in args.paths:
|
||||
try:
|
||||
mount_path = util.get_eden_mount_name(path)
|
||||
except util.NotAnEdenMountError as ex:
|
||||
print(f"error: {ex}")
|
||||
return 1
|
||||
except Exception as ex:
|
||||
print(f"error: cannot determine moint point for {path}: {ex}")
|
||||
return 1
|
||||
mounts.append(mount_path)
|
||||
|
||||
# Warn the user since this operation permanently destroys data
|
||||
if args.prompt and sys.stdin.isatty():
|
||||
mounts_list = "\n ".join(mounts)
|
||||
print(
|
||||
f"""\
|
||||
Warning: this operation will permanently delete the following checkouts:
|
||||
{mounts_list}
|
||||
|
||||
Any uncommitted changes and shelves in this checkout will be lost forever."""
|
||||
)
|
||||
if not prompt_confirmation("Proceed?"):
|
||||
print("Not confirmed")
|
||||
return 2
|
||||
|
||||
# Unmount + destroy everything
|
||||
for mount in mounts:
|
||||
print(f"Removing {mount}...")
|
||||
try:
|
||||
config.unmount(mount, delete_config=True)
|
||||
except EdenService.EdenError as ex:
|
||||
print_stderr("error: {}", ex)
|
||||
return 1
|
||||
|
||||
print(f"Success")
|
||||
return 0
|
||||
|
||||
|
||||
@subcmd("unmount", "Unmount a specific checkout")
|
||||
class UnmountCmd(Subcmd):
|
||||
|
||||
@ -527,7 +589,7 @@ class StartCmd(Subcmd):
|
||||
)
|
||||
|
||||
|
||||
def stop_aux_processes(client) -> None:
|
||||
def stop_aux_processes(client: eden.thrift.EdenClient) -> None:
|
||||
"""Tear down processes that will hold onto file handles and prevent shutdown"""
|
||||
|
||||
active_mount_points: Set[Optional[str]] = {
|
||||
@ -738,16 +800,16 @@ def create_parser() -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
if args.version:
|
||||
return do_version(args)
|
||||
if getattr(args, "func", None) is None:
|
||||
parser.print_help()
|
||||
return 0
|
||||
return_code: int = args.func(args)
|
||||
return return_code
|
||||
def prompt_confirmation(prompt: str) -> bool:
|
||||
prompt_str = f"{prompt} [y/N] "
|
||||
while True:
|
||||
response = input(prompt_str)
|
||||
value = response.lower()
|
||||
if value in ("y", "yes"):
|
||||
return True
|
||||
if value in ("", "n", "no"):
|
||||
return False
|
||||
print('Please enter "yes" or "no"')
|
||||
|
||||
|
||||
def normalize_path_arg(path_arg: str, may_need_tilde_expansion: bool = False) -> str:
|
||||
@ -766,6 +828,18 @@ def normalize_path_arg(path_arg: str, may_need_tilde_expansion: bool = False) ->
|
||||
return path_arg
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
if args.version:
|
||||
return do_version(args)
|
||||
if getattr(args, "func", None) is None:
|
||||
parser.print_help()
|
||||
return 0
|
||||
return_code: int = args.func(args)
|
||||
return return_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
retcode = main()
|
||||
sys.exit(retcode)
|
||||
|
@ -39,6 +39,15 @@ class ShutdownError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotAnEdenMountError(Exception):
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path = path
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.path} does not appear to be inside an Eden checkout"
|
||||
|
||||
|
||||
class HealthStatus(object):
|
||||
|
||||
def __init__(self, status: int, pid: Optional[int], detail: str) -> None:
|
||||
@ -423,3 +432,19 @@ def read_all(path: str) -> str:
|
||||
"""One-liner to read the contents of a file and properly close the fd."""
|
||||
with open(path, "r") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def get_eden_mount_name(path_arg: str) -> str:
|
||||
"""
|
||||
Get the path to the Eden checkout containing the specified path
|
||||
"""
|
||||
path = os.path.join(path_arg, ".eden", "root")
|
||||
try:
|
||||
return os.readlink(path)
|
||||
except OSError as ex:
|
||||
if ex.errno == errno.ENOTDIR:
|
||||
path = os.path.join(os.path.dirname(path_arg), ".eden", "root")
|
||||
return os.readlink(path)
|
||||
elif ex.errno == errno.ENOENT:
|
||||
raise NotAnEdenMountError(path_arg)
|
||||
raise
|
||||
|
@ -216,13 +216,17 @@ class BasicTest(testcase.EdenRepoTest):
|
||||
msg="attempting to run noexec.sh should fail with " "EACCES",
|
||||
)
|
||||
|
||||
def test_remove_invalid_paths(self) -> None:
|
||||
self.eden.run_unchecked("remove", "/tmp")
|
||||
self.eden.run_unchecked("remove", "/root")
|
||||
|
||||
def test_unmount(self) -> None:
|
||||
entries = set(os.listdir(self.mount))
|
||||
self.assertEqual(self.expected_mount_entries, entries)
|
||||
|
||||
self.assertTrue(self.eden.in_proc_mounts(self.mount))
|
||||
|
||||
self.eden.unmount(self.mount)
|
||||
self.eden.remove(self.mount)
|
||||
|
||||
self.assertFalse(self.eden.in_proc_mounts(self.mount))
|
||||
self.assertFalse(os.path.exists(self.mount))
|
||||
|
@ -67,7 +67,7 @@ class CloneTest(testcase.EdenRepoTest):
|
||||
empty_dir, active_mount_points, msg="mounted using the realpath"
|
||||
)
|
||||
|
||||
self.eden.run_cmd("unmount", "--destroy", symlinked_target)
|
||||
self.eden.run_cmd("remove", "--yes", symlinked_target)
|
||||
|
||||
def test_clone_to_existing_empty_directory(self) -> None:
|
||||
tmp = self._new_tmp_dir()
|
||||
|
@ -361,11 +361,11 @@ class EdenFS(object):
|
||||
params.append("--allow-empty-repo")
|
||||
self.run_cmd(*params)
|
||||
|
||||
def unmount(self, path: str) -> None:
|
||||
def remove(self, path: str) -> None:
|
||||
"""
|
||||
Run "eden unmount --destroy <path>"
|
||||
Run "eden remove <path>"
|
||||
"""
|
||||
self.run_cmd("unmount", "--destroy", path)
|
||||
self.run_cmd("remove", "--yes", path)
|
||||
|
||||
def in_proc_mounts(self, mount_path: str) -> bool:
|
||||
"""Gets all eden mounts found in /proc/mounts, and returns
|
||||
|
@ -27,9 +27,9 @@ class RCTest(testcase.EdenRepoTest):
|
||||
mounts = self.eden.list_cmd()
|
||||
self.assertEqual({self.mount: self.eden.CLIENT_ACTIVE}, mounts)
|
||||
|
||||
self.eden.unmount(self.mount)
|
||||
self.eden.remove(self.mount)
|
||||
mounts = self.eden.list_cmd()
|
||||
self.assertEqual({}, mounts, msg="There should be 0 mount paths after unmount")
|
||||
self.assertEqual({}, mounts, msg="There should be 0 mount paths after remove")
|
||||
|
||||
self.eden.clone(self.repo_name, self.mount)
|
||||
mounts = self.eden.list_cmd()
|
||||
@ -45,10 +45,10 @@ class RCTest(testcase.EdenRepoTest):
|
||||
mounts = self.eden.list_cmd()
|
||||
self.assertEqual({self.mount: self.eden.CLIENT_ACTIVE}, mounts)
|
||||
|
||||
self.eden.unmount(self.mount)
|
||||
self.eden.remove(self.mount)
|
||||
self.assertFalse(os.path.isdir(test_client_dir))
|
||||
|
||||
# Check that _remove_path_from_directory_map in unmount is successful
|
||||
# Check that _remove_path_from_directory_map in remove is successful
|
||||
mounts = self.eden.list_cmd()
|
||||
self.assertEqual({}, mounts, msg="There should be 0 paths in the directory map")
|
||||
|
||||
|
@ -85,7 +85,7 @@ class RemountTest(testcase.EdenRepoTest):
|
||||
self.eden.clone(self.repo_name, self.mount + "-" + str(i))
|
||||
|
||||
# Unmount a client
|
||||
self.eden.unmount(self.mount)
|
||||
self.eden.remove(self.mount)
|
||||
self.assertFalse(os.path.exists(self.mount))
|
||||
|
||||
self.eden.shutdown()
|
||||
@ -108,8 +108,8 @@ class RemountTest(testcase.EdenRepoTest):
|
||||
self.eden.start()
|
||||
|
||||
# Unmount clients
|
||||
self.eden.unmount(self.mount)
|
||||
self.eden.unmount(self.mount + "-3")
|
||||
self.eden.remove(self.mount)
|
||||
self.eden.remove(self.mount + "-3")
|
||||
|
||||
self.eden.shutdown()
|
||||
self.eden.start()
|
||||
|
Loading…
Reference in New Issue
Block a user