Can now install on the opensuse distribution.
- Now using the distro module to determine the linux distro.
This commit is contained in:
parent
3eb2f71adb
commit
8b867bb333
4 changed files with 87 additions and 40 deletions
|
|
@ -7,16 +7,27 @@ set -e
|
||||||
DIST_ID=$(cat /etc/os-release | grep "^ID=" | cut -d "=" -f 2)
|
DIST_ID=$(cat /etc/os-release | grep "^ID=" | cut -d "=" -f 2)
|
||||||
if [ $DIST_ID == "fedora" ]; then
|
if [ $DIST_ID == "fedora" ]; then
|
||||||
INSTALL_CMD="dnf -y install"
|
INSTALL_CMD="dnf -y install"
|
||||||
INOTIFY_NAME="python3-inotify python3-pygments python3-docopt python3-pillow"
|
DEPS="python3-distro python3-inotify python3-pygments python3-docopt python3-pillow"
|
||||||
elif [ $DIST_ID == "arch" ]; then
|
elif [ $DIST_ID == "arch" ]; then
|
||||||
INSTALL_CMD="pacman -S --noconfirm --needed"
|
INSTALL_CMD="pacman -S --noconfirm --needed"
|
||||||
INOTIFY_NAME="python-pyinotify python-pygments python-docopt python-pillow"
|
DEPS="python-pyinotify python-pygments python-docopt python-pillow"
|
||||||
|
sudo pacman -S --noconfirm python-pip
|
||||||
|
sudo pip3 install distro
|
||||||
|
elif [ $DIST_ID == "opensuse" ]; then
|
||||||
|
INSTALL_CMD="zypper -n install"
|
||||||
|
DEPS="python3-pyinotify python3-Pygments python3-docopt python3-Pillow"
|
||||||
|
sudo pip3 install distro
|
||||||
|
elif [ $DIST_ID == "debian" ]; then
|
||||||
|
INSTALL_CMD="apt --yes install"
|
||||||
|
DEPS="python3-pyinotify python3-pygments python3-docopt python3-pillow"
|
||||||
|
sudo apt --yes install python3-pip
|
||||||
|
sudo pip3 install distro
|
||||||
else
|
else
|
||||||
INSTALL_CMD="apt --yes install"
|
INSTALL_CMD="apt --yes install"
|
||||||
INOTIFY_NAME="python3-pyinotify python3-pygments python3-docopt python3-pillow"
|
DEPS="python3-distro python3-pyinotify python3-pygments python3-docopt python3-pillow"
|
||||||
fi
|
fi
|
||||||
echo "Install the dependencies of the vigil script..."
|
echo "Installing the dependencies of the vigil script..."
|
||||||
sudo $INSTALL_CMD $INOTIFY_NAME util-linux
|
sudo $INSTALL_CMD $DEPS util-linux
|
||||||
echo
|
echo
|
||||||
echo "Install all the tools vigil may need..."
|
echo "Installing all the tools vigil may need..."
|
||||||
./install-tools
|
./install-tools
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
|
||||||
import platform
|
import distro
|
||||||
import subprocess
|
import subprocess
|
||||||
import tools
|
import tools
|
||||||
|
|
||||||
|
|
||||||
dist_id = platform.linux_distribution()[0].lower()
|
dist_id = distro.id()
|
||||||
pip_deps, pip3_deps, dist_deps = set(), set(), set()
|
pip_deps, pip3_deps, dist_deps = set(), set(), set()
|
||||||
for dependency in tools.dependencies(dist_id):
|
for dependency in tools.dependencies(dist_id):
|
||||||
if "/" in dependency:
|
if "/" in dependency:
|
||||||
|
|
@ -17,7 +17,8 @@ for dependency in tools.dependencies(dist_id):
|
||||||
cmd_for_dist = {"ubuntu": ["apt-get", "-y", "install"],
|
cmd_for_dist = {"ubuntu": ["apt-get", "-y", "install"],
|
||||||
"debian": ["apt-get", "-y", "install"],
|
"debian": ["apt-get", "-y", "install"],
|
||||||
"fedora": ["dnf", "-y", "install"],
|
"fedora": ["dnf", "-y", "install"],
|
||||||
"arch": ["pacman", "-S", "--noconfirm", "--needed"]}
|
"arch": ["pacman", "-S", "--noconfirm", "--needed"],
|
||||||
|
"opensuse": ["zypper", "-n", "install"]}
|
||||||
if pip_deps:
|
if pip_deps:
|
||||||
dist_deps.add("python2-pip" if dist_id == "arch" else "python-pip")
|
dist_deps.add("python2-pip" if dist_id == "arch" else "python-pip")
|
||||||
if pip3_deps:
|
if pip3_deps:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,21 @@ set -e
|
||||||
VIGIL_PATH=$(realpath $(dirname $0))
|
VIGIL_PATH=$(realpath $(dirname $0))
|
||||||
|
|
||||||
|
|
||||||
|
function mount_squashfs_iso {
|
||||||
|
mkdir iso && sudo mount -o loop $1 iso
|
||||||
|
mkdir lower && sudo mount -t squashfs iso/$2 lower
|
||||||
|
mkdir upper work $3 && sudo mount -t overlay \
|
||||||
|
-o lowerdir=lower,upperdir=upper,workdir=work overlay $3
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function umount_squashfs_iso {
|
||||||
|
sudo umount $1 && sudo rm -rf upper work $1
|
||||||
|
sudo umount lower && rmdir lower
|
||||||
|
sudo umount iso && rmdir iso
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function run_in_container {
|
function run_in_container {
|
||||||
CONTAINER=$1
|
CONTAINER=$1
|
||||||
shift
|
shift
|
||||||
|
|
@ -78,18 +93,28 @@ function remove_archlinux {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
PIXEL_ISO=2016-12-13-pixel-x86-jessie.iso
|
OPENSUSE_ISO="openSUSE-Tumbleweed-GNOME-Live-x86_64-Current.iso"
|
||||||
|
|
||||||
|
|
||||||
|
function build_opensuse {
|
||||||
|
wget --continue "https://download.opensuse.org/tumbleweed/iso/$OPENSUSE_ISO"
|
||||||
|
mount_squashfs_iso $OPENSUSE_ISO \
|
||||||
|
"openSUSE-tumbleweed-livecd-gnome-read-only.x86_64-2.8.0" opensuse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function remove_opensuse {
|
||||||
|
umount_squashfs_iso opensuse
|
||||||
|
rm $OPENSUSE_ISO
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
PIXEL_ISO="2016-12-13-pixel-x86-jessie.iso"
|
||||||
|
|
||||||
|
|
||||||
function build_pixel {
|
function build_pixel {
|
||||||
wget --continue http://downloads.raspberrypi.org/pixel_x86/images/pixel_x86-2016-12-13/$PIXEL_ISO
|
wget --continue "http://downloads.raspberrypi.org/pixel_x86/images/pixel_x86-2016-12-13/$PIXEL_ISO"
|
||||||
mkdir iso
|
mount_squashfs_iso $PIXEL_ISO live/filesystem.squashfs pixel
|
||||||
sudo mount -o loop $PIXEL_ISO iso
|
|
||||||
mkdir lower
|
|
||||||
sudo mount -t squashfs iso/live/filesystem.squashfs lower
|
|
||||||
mkdir upper work pixel
|
|
||||||
sudo mount -t overlay -o lowerdir=lower,upperdir=upper,workdir=work \
|
|
||||||
overlay pixel
|
|
||||||
sudo rm pixel/etc/resolv.conf
|
sudo rm pixel/etc/resolv.conf
|
||||||
echo "nameserver 127.0.0.53" | sudo dd of=pixel/etc/resolv.conf
|
echo "nameserver 127.0.0.53" | sudo dd of=pixel/etc/resolv.conf
|
||||||
run_in_container pixel apt-get update
|
run_in_container pixel apt-get update
|
||||||
|
|
@ -97,9 +122,7 @@ function build_pixel {
|
||||||
|
|
||||||
|
|
||||||
function remove_pixel {
|
function remove_pixel {
|
||||||
sudo umount pixel && sudo rm -rf upper work pixel
|
umount_squashfs_iso pixel
|
||||||
sudo umount lower && rmdir lower
|
|
||||||
sudo umount iso && rmdir iso
|
|
||||||
rm $PIXEL_ISO
|
rm $PIXEL_ISO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +130,7 @@ function remove_pixel {
|
||||||
[ $# -eq 0 ] && WORK_PATH=$(mktemp -d --suffix=-vigil) || WORK_PATH="$1"
|
[ $# -eq 0 ] && WORK_PATH=$(mktemp -d --suffix=-vigil) || WORK_PATH="$1"
|
||||||
sudo apt-get install -y systemd-container debootstrap xz-utils wget
|
sudo apt-get install -y systemd-container debootstrap xz-utils wget
|
||||||
cd $WORK_PATH
|
cd $WORK_PATH
|
||||||
for DISTRIBUTION in ubuntu fedora debian archlinux pixel; do
|
for DISTRIBUTION in ubuntu fedora debian archlinux opensuse pixel; do
|
||||||
if [ -e $DISTRIBUTION ]; then
|
if [ -e $DISTRIBUTION ]; then
|
||||||
echo "$DISTRIBUTION container already exists."
|
echo "$DISTRIBUTION container already exists."
|
||||||
else
|
else
|
||||||
|
|
|
||||||
46
tools.py
46
tools.py
|
|
@ -16,7 +16,6 @@ import math
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import pickle
|
import pickle
|
||||||
import platform
|
|
||||||
import pwd
|
import pwd
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
@ -24,6 +23,7 @@ import tempfile
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
import distro
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
import pygments
|
import pygments
|
||||||
import pygments.lexers
|
import pygments.lexers
|
||||||
|
|
@ -270,7 +270,8 @@ def metadata(path):
|
||||||
return (Status.normal, fill3.Text(fill3.join("", text)))
|
return (Status.normal, fill3.Text(fill3.join("", text)))
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"python3-pygments"}, arch_deps={"python-pygments"})
|
@deps(deps={"python3-pygments"}, arch_deps={"python-pygments"},
|
||||||
|
opensuse_deps={"python3-Pygments"})
|
||||||
def contents(path):
|
def contents(path):
|
||||||
root, ext = splitext(path)
|
root, ext = splitext(path)
|
||||||
if ext == "":
|
if ext == "":
|
||||||
|
|
@ -349,8 +350,9 @@ def pydoc(path):
|
||||||
return status, fill3.Text(output)
|
return status, fill3.Text(output)
|
||||||
|
|
||||||
|
|
||||||
@deps(deps="mypy", url="mypy", fedora_deps={"python3-mypy"},
|
@deps(deps={"mypy"}, url="mypy", fedora_deps={"python3-mypy"},
|
||||||
debian_deps={"pip3/mypy"}, arch_deps={"pip3/mypy"}, executables={"mypy"})
|
debian_deps={"pip3/mypy"}, arch_deps={"pip3/mypy"},
|
||||||
|
opensuse_deps={"pip3/mypy"}, executables={"mypy"})
|
||||||
def mypy(path):
|
def mypy(path):
|
||||||
stdout, stderr, returncode = _do_command(["mypy", path], timeout=TIMEOUT)
|
stdout, stderr, returncode = _do_command(["mypy", path], timeout=TIMEOUT)
|
||||||
status = Status.ok if returncode == 0 else Status.normal
|
status = Status.ok if returncode == 0 else Status.normal
|
||||||
|
|
@ -366,6 +368,7 @@ def _colorize_coverage_report(text):
|
||||||
|
|
||||||
@deps(deps={"python-coverage", "python3-coverage"},
|
@deps(deps={"python-coverage", "python3-coverage"},
|
||||||
arch_deps={"python2-coverage", "python-coverage"},
|
arch_deps={"python2-coverage", "python-coverage"},
|
||||||
|
opensuse_deps={"python2-coverage", "python3-coverage"},
|
||||||
url="python3-coverage")
|
url="python3-coverage")
|
||||||
def python_coverage(path):
|
def python_coverage(path):
|
||||||
# FIX: Also use test_*.py files.
|
# FIX: Also use test_*.py files.
|
||||||
|
|
@ -403,26 +406,28 @@ def python_profile(path):
|
||||||
fedora_deps={"python2-pycodestyle", "python3-pycodestyle"},
|
fedora_deps={"python2-pycodestyle", "python3-pycodestyle"},
|
||||||
debian_deps={"pip/pycodestyle", "pip3/pycodestyle"},
|
debian_deps={"pip/pycodestyle", "pip3/pycodestyle"},
|
||||||
arch_deps={"python-pycodestyle", "python2-pycodestyle"},
|
arch_deps={"python-pycodestyle", "python2-pycodestyle"},
|
||||||
|
opensuse_deps={"python2-pycodestyle", "python3-pycodestyle"},
|
||||||
url="python-pycodestyle")
|
url="python-pycodestyle")
|
||||||
def pycodestyle(path):
|
def pycodestyle(path):
|
||||||
return _run_command([_python_version(path), "-m", "pycodestyle", path])
|
return _run_command([_python_version(path), "-m", "pycodestyle", path])
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"pyflakes"}, arch_deps={"python2-pyflakes", "python-pyflakes"},
|
@deps(deps={"pyflakes"}, arch_deps={"python2-pyflakes", "python-pyflakes"},
|
||||||
url="pyflakes")
|
opensuse_deps={"python2-pyflakes", "python3-pyflakes"}, url="pyflakes")
|
||||||
def pyflakes(path):
|
def pyflakes(path):
|
||||||
return _run_command([_python_version(path), "-m", "pyflakes", path])
|
return _run_command([_python_version(path), "-m", "pyflakes", path])
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"pylint", "pylint3"}, fedora_deps={"pylint", "python3-pylint"},
|
@deps(deps={"pylint", "pylint3"}, fedora_deps={"pylint", "python3-pylint"},
|
||||||
arch_deps={"python2-pylint", "python-pylint"},
|
arch_deps={"python2-pylint", "python-pylint"},
|
||||||
|
opensuse_deps={"python2-pylint", "python3-pylint"},
|
||||||
debian_deps={"pip/pylint", "pip3/pylint"}, url="pylint3")
|
debian_deps={"pip/pylint", "pip3/pylint"}, url="pylint3")
|
||||||
def pylint(path):
|
def pylint(path):
|
||||||
return _run_command([_python_version(path), "-m", "pylint",
|
return _run_command([_python_version(path), "-m", "pylint",
|
||||||
"--errors-only", path])
|
"--errors-only", path])
|
||||||
|
|
||||||
|
|
||||||
@deps(deps=set(), url="https://github.com/ahamilton/vigil/blob/master/gut.py")
|
@deps(url="https://github.com/ahamilton/vigil/blob/master/gut.py")
|
||||||
def python_gut(path):
|
def python_gut(path):
|
||||||
with open(path) as module_file:
|
with open(path) as module_file:
|
||||||
output = gut.gut_module(module_file.read())
|
output = gut.gut_module(module_file.read())
|
||||||
|
|
@ -450,7 +455,8 @@ def _colorize_mccabe(text, python_version):
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"python-mccabe", "python3-mccabe"},
|
@deps(deps={"python-mccabe", "python3-mccabe"},
|
||||||
arch_deps={"python2-mccabe", "python-mccabe"}, url="python3-mccabe")
|
arch_deps={"python2-mccabe", "python-mccabe"},
|
||||||
|
opensuse_deps={"python2-mccabe", "python3-mccabe"}, url="python3-mccabe")
|
||||||
def python_mccabe(path):
|
def python_mccabe(path):
|
||||||
python_version = _python_version(path)
|
python_version = _python_version(path)
|
||||||
stdout, *rest = _do_command([python_version, "-m", "mccabe", path])
|
stdout, *rest = _do_command([python_version, "-m", "mccabe", path])
|
||||||
|
|
@ -467,7 +473,7 @@ def python_tidy(path): # Deps: found on internet?
|
||||||
return Status.normal, _syntax_highlight_using_path(stdout, path)
|
return Status.normal, _syntax_highlight_using_path(stdout, path)
|
||||||
|
|
||||||
|
|
||||||
@deps(deps=set(), url="https://docs.python.org/3/library/dis.html")
|
@deps(url="https://docs.python.org/3/library/dis.html")
|
||||||
def disassemble_pyc(path):
|
def disassemble_pyc(path):
|
||||||
with open(path, "rb") as file_:
|
with open(path, "rb") as file_:
|
||||||
bytecode = file_.read()
|
bytecode = file_.read()
|
||||||
|
|
@ -479,6 +485,7 @@ def disassemble_pyc(path):
|
||||||
|
|
||||||
@deps(deps={"python-bandit", "python3-bandit"}, fedora_deps={"bandit"},
|
@deps(deps={"python-bandit", "python3-bandit"}, fedora_deps={"bandit"},
|
||||||
debian_deps={"pip/bandit", "pip3/bandit"}, arch_deps={"bandit"},
|
debian_deps={"pip/bandit", "pip3/bandit"}, arch_deps={"bandit"},
|
||||||
|
opensuse_deps={"pip/bandit", "pip3/bandit"},
|
||||||
url="python3-bandit")
|
url="python3-bandit")
|
||||||
def bandit(path):
|
def bandit(path):
|
||||||
python_version = _python_version(path)
|
python_version = _python_version(path)
|
||||||
|
|
@ -512,6 +519,7 @@ def perldoc(path):
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"perltidy"}, arch_deps={"perl-test-perltidy"},
|
@deps(deps={"perltidy"}, arch_deps={"perl-test-perltidy"},
|
||||||
|
opensuse_deps={"perl-Test-PerlTidy"},
|
||||||
url="http://perltidy.sourceforge.net/", executables={"perltidy"})
|
url="http://perltidy.sourceforge.net/", executables={"perltidy"})
|
||||||
def perltidy(path):
|
def perltidy(path):
|
||||||
stdout, *rest = _do_command(["perltidy", "-st", path])
|
stdout, *rest = _do_command(["perltidy", "-st", path])
|
||||||
|
|
@ -585,7 +593,7 @@ def nm(path):
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"python-pdfminer"}, arch_deps=set(), url="python-pdfminer",
|
@deps(deps={"python-pdfminer"}, arch_deps=set(), url="python-pdfminer",
|
||||||
executables={"pdf2txt"}, missing_in={"arch", "fedora"})
|
executables={"pdf2txt"}, missing_in={"arch", "fedora", "opensuse"})
|
||||||
def pdf2txt(path):
|
def pdf2txt(path):
|
||||||
return _run_command(["pdf2txt", path], Status.normal)
|
return _run_command(["pdf2txt", path], Status.normal)
|
||||||
|
|
||||||
|
|
@ -621,7 +629,7 @@ def cpp_syntax_clang(path):
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"bcpp"}, fedora_deps=set(), arch_deps=set(), executables={"bcpp"},
|
@deps(deps={"bcpp"}, fedora_deps=set(), arch_deps=set(), executables={"bcpp"},
|
||||||
missing_in={"arch", "fedora"})
|
missing_in={"arch", "fedora", "opensuse"})
|
||||||
def bcpp(path):
|
def bcpp(path):
|
||||||
stdout, stderr, returncode = _do_command(["bcpp", "-fi", path])
|
stdout, stderr, returncode = _do_command(["bcpp", "-fi", path])
|
||||||
status = Status.normal if returncode == 0 else Status.problem
|
status = Status.normal if returncode == 0 else Status.problem
|
||||||
|
|
@ -642,8 +650,9 @@ def uncrustify(path):
|
||||||
return status, _syntax_highlight_using_path(stdout, path)
|
return status, _syntax_highlight_using_path(stdout, path)
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"php"}, url="https://en.wikipedia.org/wiki/PHP",
|
@deps(deps={"php"}, opensuse_deps={"php5"},
|
||||||
executables={"php"}, missing_in={"debian"})
|
url="https://en.wikipedia.org/wiki/PHP", executables={"php"},
|
||||||
|
missing_in={"debian"})
|
||||||
def php5_syntax(path):
|
def php5_syntax(path):
|
||||||
return _run_command(["php", "--syntax-check", path])
|
return _run_command(["php", "--syntax-check", path])
|
||||||
|
|
||||||
|
|
@ -665,7 +674,8 @@ def _resize_image(image, new_width):
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"python3-pil"}, fedora_deps={"python3-pillow"},
|
@deps(deps={"python3-pil"}, fedora_deps={"python3-pillow"},
|
||||||
arch_deps={"python-pillow"}, url="python3-pil")
|
arch_deps={"python-pillow"}, opensuse_deps={"python3-Pillow"},
|
||||||
|
url="python3-pil")
|
||||||
def pil(path):
|
def pil(path):
|
||||||
with open(path, "rb") as image_file:
|
with open(path, "rb") as image_file:
|
||||||
with PIL.Image.open(image_file).convert("RGB") as image:
|
with PIL.Image.open(image_file).convert("RGB") as image:
|
||||||
|
|
@ -683,7 +693,8 @@ def pil(path):
|
||||||
|
|
||||||
|
|
||||||
@deps(deps={"python3-pil"}, fedora_deps={"python3-pillow"},
|
@deps(deps={"python3-pil"}, fedora_deps={"python3-pillow"},
|
||||||
arch_deps={"python-pillow"}, url="python3-pil")
|
arch_deps={"python-pillow"}, opensuse_deps={"python3-Pillow"},
|
||||||
|
url="python3-pil")
|
||||||
def pil_half(path):
|
def pil_half(path):
|
||||||
with open(path, "rb") as image_file:
|
with open(path, "rb") as image_file:
|
||||||
with PIL.Image.open(image_file).convert("RGB") as image:
|
with PIL.Image.open(image_file).convert("RGB") as image:
|
||||||
|
|
@ -863,7 +874,7 @@ def is_tool_in_distribution(tool, distribution):
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=1)
|
@functools.lru_cache(maxsize=1)
|
||||||
def _tools_for_extension():
|
def _tools_for_extension():
|
||||||
distribution = platform.linux_distribution()[0].lower()
|
distribution = distro.id()
|
||||||
result = {}
|
result = {}
|
||||||
for extensions, tools in TOOLS_FOR_EXTENSIONS:
|
for extensions, tools in TOOLS_FOR_EXTENSIONS:
|
||||||
for extension in extensions:
|
for extension in extensions:
|
||||||
|
|
@ -880,12 +891,13 @@ def tools_all():
|
||||||
|
|
||||||
|
|
||||||
def tool_dependencies(tool, distribution="ubuntu"):
|
def tool_dependencies(tool, distribution="ubuntu"):
|
||||||
if distribution not in ["ubuntu", "debian", "fedora", "arch"]:
|
|
||||||
raise ValueError
|
|
||||||
try:
|
try:
|
||||||
return getattr(tool, distribution + "_deps")
|
return getattr(tool, distribution + "_deps")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
try:
|
||||||
return tool.deps
|
return tool.deps
|
||||||
|
except AttributeError:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def dependencies(distribution="ubuntu"):
|
def dependencies(distribution="ubuntu"):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue