mirror of
https://github.com/Jguer/yay.git
synced 2024-11-07 01:27:21 +01:00
Merge pull request #1779 from Jguer/jguer/migrate-provides
Config: Add migration service and migrate provides default value
This commit is contained in:
commit
e2f61255cb
2
.github/workflows/builder-image.yml
vendored
2
.github/workflows/builder-image.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
DOCKER_BUILDKIT: 0
|
||||
COMPOSE_DOCKER_CLI_BUILD: 0
|
||||
with:
|
||||
platforms: linux/amd64, linux/arm/v6,linux/arm/v7,linux/arm64
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
file: ci.Dockerfile
|
||||
push: true
|
||||
tags: jguer/yay-builder:latest
|
||||
|
14
.github/workflows/multiarch-build.yml
vendored
14
.github/workflows/multiarch-build.yml
vendored
@ -11,7 +11,6 @@ jobs:
|
||||
arch:
|
||||
[
|
||||
"linux/amd64 x86_64",
|
||||
"linux/arm/v6 armv6h",
|
||||
"linux/arm/v7 armv7h",
|
||||
"linux/arm64 aarch64",
|
||||
]
|
||||
@ -73,9 +72,6 @@ jobs:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: yay_armv7h
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: yay_armv6h
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: yay_aarch64
|
||||
@ -109,16 +105,6 @@ jobs:
|
||||
asset_path: ./yay_${{ steps.tags.outputs.version }}_armv7h.tar.gz
|
||||
asset_name: yay_${{ steps.tags.outputs.version }}_armv7h.tar.gz
|
||||
asset_content_type: application/tar+gzip
|
||||
- name: Upload armv6h asset
|
||||
id: upload-release-asset-armv6h
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./yay_${{ steps.tags.outputs.version }}_armv6h.tar.gz
|
||||
asset_name: yay_${{ steps.tags.outputs.version }}_armv6h.tar.gz
|
||||
asset_content_type: application/tar+gzip
|
||||
- name: Upload aarch64 asset
|
||||
id: upload-release-asset-aarch64
|
||||
uses: actions/upload-release-asset@v1
|
||||
|
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ PREFIX := /usr/local
|
||||
|
||||
MAJORVERSION := 11
|
||||
MINORVERSION := 2
|
||||
PATCHVERSION := 0
|
||||
PATCHVERSION := 1
|
||||
VERSION ?= ${MAJORVERSION}.${MINORVERSION}.${PATCHVERSION}
|
||||
|
||||
LOCALEDIR := po
|
||||
|
@ -1,11 +1,11 @@
|
||||
FROM docker.io/lopsided/archlinux:devel
|
||||
FROM docker.io/jguer/yay-builder:latest
|
||||
|
||||
ENV GO111MODULE=on
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod .
|
||||
|
||||
RUN pacman -Syu --overwrite=* --needed --noconfirm go git && \
|
||||
RUN pacman -Sy && pacman -S --overwrite=* --noconfirm archlinux-keyring && pacman -Su --overwrite=* --needed --noconfirm go git && \
|
||||
rm -rfv /var/cache/pacman/* /var/lib/pacman/sync/* && \
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.44.2 && \
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.48.0 && \
|
||||
go mod download
|
||||
|
@ -550,7 +550,7 @@ func pkgbuildsToSkip(bases []dep.Base, targets stringset.StringSet) stringset.St
|
||||
pkgbuild, err := gosrc.ParseFile(dir)
|
||||
|
||||
if err == nil {
|
||||
if alpm.VerCmp(pkgbuild.Version(), base.Version()) >= 0 {
|
||||
if db.VerCmp(pkgbuild.Version(), base.Version()) >= 0 {
|
||||
toSkip.Set(base.Pkgbase())
|
||||
}
|
||||
}
|
||||
@ -812,7 +812,8 @@ func doInstall(ctx context.Context, arguments, cmdArgs *parser.Arguments, pkgDep
|
||||
|
||||
func doAddTarget(dp *dep.Pool, localNamesCache, remoteNamesCache stringset.StringSet,
|
||||
arguments, cmdArgs *parser.Arguments, pkgdests map[string]string,
|
||||
deps, exp []string, name string, optional bool) (newDeps, newExp []string, err error) {
|
||||
deps, exp []string, name string, optional bool,
|
||||
) (newDeps, newExp []string, err error) {
|
||||
pkgdest, ok := pkgdests[name]
|
||||
if !ok {
|
||||
if optional {
|
||||
|
5
main.go
5
main.go
@ -114,6 +114,11 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if errS := config.RunMigrations(
|
||||
settings.DefaultMigrations(), config.Runtime.ConfigPath); errS != nil {
|
||||
text.Errorln(errS)
|
||||
}
|
||||
|
||||
cmdArgs := parser.MakeArguments()
|
||||
|
||||
if err = config.ParseCommandLine(cmdArgs); err != nil {
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@ -45,7 +45,7 @@ func (m *mockDoer) Do(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(m.t, m.wantUrl, req.URL.String())
|
||||
return &http.Response{
|
||||
StatusCode: m.returnStatusCode,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(m.returnBody)),
|
||||
Body: io.NopCloser(bytes.NewBufferString(m.returnBody)),
|
||||
}, m.returnErr
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,10 @@ type (
|
||||
Depend = alpm.Depend
|
||||
)
|
||||
|
||||
func VerCmp(a, b string) int {
|
||||
return alpm.VerCmp(a, b)
|
||||
// VerCmp performs version comparison according to Pacman conventions. Return
|
||||
// value is <0 if and only if v1 is older than v2.
|
||||
func VerCmp(v1, v2 string) int {
|
||||
return alpm.VerCmp(v1, v2)
|
||||
}
|
||||
|
||||
type Upgrade struct {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
aur "github.com/Jguer/aur"
|
||||
gosrc "github.com/Morganamilo/go-srcinfo"
|
||||
@ -59,7 +60,7 @@ func getPgpKey(key string) string {
|
||||
}
|
||||
|
||||
func startPgpKeyServer() *http.Server {
|
||||
srv := &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", gpgServerPort)}
|
||||
srv := &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", gpgServerPort), ReadHeaderTimeout: 1 * time.Second}
|
||||
|
||||
go func() {
|
||||
err := srv.ListenAndServe()
|
||||
|
@ -3,7 +3,7 @@ package query
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -83,7 +83,7 @@ type mockDoer struct{}
|
||||
func (m *mockDoer) Do(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewBufferString(validPayload)),
|
||||
Body: io.NopCloser(bytes.NewBufferString(validPayload)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -213,7 +213,7 @@ func DefaultConfig(version string) *Configuration {
|
||||
AnswerEdit: "",
|
||||
AnswerUpgrade: "",
|
||||
RemoveMake: "ask",
|
||||
Provides: true,
|
||||
Provides: false,
|
||||
UpgradeMenu: true,
|
||||
CleanMenu: true,
|
||||
DiffMenu: true,
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// GIVEN no user directories and sudo user
|
||||
@ -13,11 +14,15 @@ import (
|
||||
// THEN the selected cache home should be in the tmp dir
|
||||
func Test_getCacheHome(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.Unsetenv("XDG_CACHE_HOME")
|
||||
os.Unsetenv("HOME")
|
||||
os.Setenv("SUDO_USER", "test")
|
||||
os.Setenv("TMPDIR", dir)
|
||||
require.NoError(t, os.Unsetenv("XDG_CACHE_HOME"))
|
||||
require.NoError(t, os.Unsetenv("HOME"))
|
||||
require.NoError(t, os.Setenv("SUDO_USER", "test"))
|
||||
require.NoError(t, os.Setenv("TMPDIR", dir))
|
||||
|
||||
got, err := getCacheHome()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(dir, "yay"), got)
|
||||
|
||||
require.NoError(t, os.Unsetenv("TMPDIR"))
|
||||
require.NoError(t, os.Unsetenv("SUDO_USER"))
|
||||
}
|
||||
|
67
pkg/settings/migrations.go
Normal file
67
pkg/settings/migrations.go
Normal file
@ -0,0 +1,67 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Jguer/yay/v11/pkg/db"
|
||||
"github.com/Jguer/yay/v11/pkg/text"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
type configMigration interface {
|
||||
// Description of what the migration does
|
||||
fmt.Stringer
|
||||
// return true if migration was done
|
||||
Do(config *Configuration) bool
|
||||
// Target version of the migration (e.g. "11.2.1")
|
||||
// Should match the version of yay releasing this migration
|
||||
TargetVersion() string
|
||||
}
|
||||
|
||||
type configProviderMigration struct{}
|
||||
|
||||
func (migration *configProviderMigration) String() string {
|
||||
return gotext.Get("Disable 'provides' setting by default")
|
||||
}
|
||||
|
||||
func (migration *configProviderMigration) Do(config *Configuration) bool {
|
||||
if config.Provides {
|
||||
config.Provides = false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (migration *configProviderMigration) TargetVersion() string {
|
||||
return "11.2.1"
|
||||
}
|
||||
|
||||
func DefaultMigrations() []configMigration {
|
||||
return []configMigration{
|
||||
&configProviderMigration{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Configuration) RunMigrations(migrations []configMigration, configPath string) error {
|
||||
saveConfig := false
|
||||
|
||||
for _, migration := range migrations {
|
||||
if db.VerCmp(migration.TargetVersion(), c.Version) > 0 {
|
||||
if migration.Do(c) {
|
||||
text.Infoln("Config migration executed (",
|
||||
migration.TargetVersion(), "):", migration)
|
||||
|
||||
saveConfig = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if saveConfig {
|
||||
return c.Save(configPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
151
pkg/settings/migrations_test.go
Normal file
151
pkg/settings/migrations_test.go
Normal file
@ -0,0 +1,151 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMigrationNothingToDo(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create temporary file for config
|
||||
configFile, err := os.CreateTemp("/tmp", "yay-*-config.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
testFilePath := configFile.Name()
|
||||
defer os.Remove(testFilePath)
|
||||
// Create config with configVersion
|
||||
config := Configuration{
|
||||
Version: "99.0.0",
|
||||
// Create runtime with runtimeVersion
|
||||
Runtime: &Runtime{Version: "20.0.0"},
|
||||
}
|
||||
|
||||
// Run Migration
|
||||
err = config.RunMigrations(DefaultMigrations(), testFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check file contents if wantSave otherwise check file empty
|
||||
cfile, err := os.Open(testFilePath)
|
||||
require.NoError(t, err)
|
||||
defer cfile.Close()
|
||||
|
||||
decoder := json.NewDecoder(cfile)
|
||||
newConfig := Configuration{}
|
||||
err = decoder.Decode(&newConfig)
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, newConfig.Version)
|
||||
}
|
||||
|
||||
func TestProvidesMigrationDo(t *testing.T) {
|
||||
migration := &configProviderMigration{}
|
||||
config := Configuration{Provides: true}
|
||||
|
||||
assert.True(t, migration.Do(&config))
|
||||
|
||||
falseConfig := Configuration{Provides: false}
|
||||
|
||||
assert.False(t, migration.Do(&falseConfig))
|
||||
}
|
||||
|
||||
func TestProvidesMigration(t *testing.T) {
|
||||
t.Parallel()
|
||||
type testCase struct {
|
||||
desc string
|
||||
testConfig *Configuration
|
||||
wantSave bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
desc: "to upgrade",
|
||||
testConfig: &Configuration{
|
||||
Version: "11.0.1",
|
||||
Runtime: &Runtime{Version: "11.2.1"},
|
||||
Provides: true,
|
||||
},
|
||||
wantSave: true,
|
||||
},
|
||||
{
|
||||
desc: "to upgrade-git",
|
||||
testConfig: &Configuration{
|
||||
Version: "11.2.0.r7.g6f60892",
|
||||
Runtime: &Runtime{Version: "11.2.1"},
|
||||
Provides: true,
|
||||
},
|
||||
wantSave: true,
|
||||
},
|
||||
{
|
||||
desc: "to not upgrade",
|
||||
testConfig: &Configuration{
|
||||
Version: "11.2.0",
|
||||
Runtime: &Runtime{Version: "11.2.1"},
|
||||
Provides: false,
|
||||
},
|
||||
wantSave: false,
|
||||
},
|
||||
{
|
||||
desc: "to not upgrade - target version",
|
||||
testConfig: &Configuration{
|
||||
Version: "11.2.1",
|
||||
Runtime: &Runtime{Version: "11.2.1"},
|
||||
Provides: true,
|
||||
},
|
||||
wantSave: false,
|
||||
},
|
||||
{
|
||||
desc: "to not upgrade - new version",
|
||||
testConfig: &Configuration{
|
||||
Version: "11.3.0",
|
||||
Runtime: &Runtime{Version: "11.3.0"},
|
||||
Provides: true,
|
||||
},
|
||||
wantSave: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
// Create temporary file for config
|
||||
configFile, err := os.CreateTemp("/tmp", "yay-*-config.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
testFilePath := configFile.Name()
|
||||
defer os.Remove(testFilePath)
|
||||
// Create config with configVersion and provides
|
||||
tcConfig := Configuration{
|
||||
Version: tc.testConfig.Version,
|
||||
Provides: tc.testConfig.Provides,
|
||||
// Create runtime with runtimeVersion
|
||||
Runtime: &Runtime{Version: tc.testConfig.Runtime.Version},
|
||||
}
|
||||
|
||||
// Run Migration
|
||||
err = tcConfig.RunMigrations(
|
||||
[]configMigration{&configProviderMigration{}},
|
||||
testFilePath)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check file contents if wantSave otherwise check file empty
|
||||
cfile, err := os.Open(testFilePath)
|
||||
require.NoError(t, err)
|
||||
defer cfile.Close()
|
||||
|
||||
decoder := json.NewDecoder(cfile)
|
||||
newConfig := Configuration{}
|
||||
err = decoder.Decode(&newConfig)
|
||||
if tc.wantSave {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.testConfig.Runtime.Version, newConfig.Version)
|
||||
assert.Equal(t, false, newConfig.Provides)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
assert.Empty(t, newConfig.Version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ type OriginInfoByURL map[string]OriginInfo
|
||||
|
||||
// OriginInfo contains the last commit sha of a repo
|
||||
// Example:
|
||||
//
|
||||
// "github.com/Jguer/yay.git": {
|
||||
// "protocols": [
|
||||
// "https"
|
||||
@ -90,7 +91,8 @@ func (v *InfoStore) getCommit(ctx context.Context, url, branch string, protocols
|
||||
}
|
||||
|
||||
func (v *InfoStore) Update(ctx context.Context, pkgName string,
|
||||
sources []gosrc.ArchString, mux sync.Locker, wg *sync.WaitGroup) {
|
||||
sources []gosrc.ArchString, mux sync.Locker, wg *sync.WaitGroup,
|
||||
) {
|
||||
defer wg.Done()
|
||||
|
||||
info := make(OriginInfoByURL)
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
gosrc "github.com/Morganamilo/go-srcinfo"
|
||||
"github.com/bradleyjkemp/cupaloy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Jguer/yay/v11/pkg/settings/exe"
|
||||
)
|
||||
@ -261,9 +262,9 @@ func TestInfoStore_Update(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
file, err := os.CreateTemp("/tmp", "yay-vcs-test")
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(file.Name())
|
||||
file, err := os.CreateTemp("/tmp", "yay-infostore-*-test")
|
||||
filePath := file.Name()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
@ -271,7 +272,7 @@ func TestInfoStore_Update(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := &InfoStore{
|
||||
OriginsByPackage: tt.fields.OriginsByPackage,
|
||||
FilePath: file.Name(),
|
||||
FilePath: filePath,
|
||||
CmdBuilder: tt.fields.CmdBuilder,
|
||||
}
|
||||
var mux sync.Mutex
|
||||
@ -296,6 +297,8 @@ func TestInfoStore_Update(t *testing.T) {
|
||||
cupaloy.SnapshotT(t, marshalledinfo)
|
||||
})
|
||||
}
|
||||
|
||||
require.NoError(t, os.Remove(filePath))
|
||||
}
|
||||
|
||||
func TestInfoStore_Remove(t *testing.T) {
|
||||
@ -325,9 +328,9 @@ func TestInfoStore_Remove(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
file, err := os.CreateTemp("/tmp", "yay-vcs-test")
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(file.Name())
|
||||
file, err := os.CreateTemp("/tmp", "yay-vcs-*-test")
|
||||
filePath := file.Name()
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
@ -335,10 +338,12 @@ func TestInfoStore_Remove(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := &InfoStore{
|
||||
OriginsByPackage: tt.fields.OriginsByPackage,
|
||||
FilePath: file.Name(),
|
||||
FilePath: filePath,
|
||||
}
|
||||
v.RemovePackage(tt.args.pkgs)
|
||||
assert.Len(t, tt.fields.OriginsByPackage, 2)
|
||||
})
|
||||
}
|
||||
|
||||
require.NoError(t, os.Remove(filePath))
|
||||
}
|
||||
|
11
upgrade.go
11
upgrade.go
@ -35,7 +35,8 @@ func filterUpdateList(list []db.Upgrade, filter upgrade.Filter) []db.Upgrade {
|
||||
|
||||
// upList returns lists of packages to upgrade from each source.
|
||||
func upList(ctx context.Context, warnings *query.AURWarnings, dbExecutor db.Executor, enableDowngrade bool,
|
||||
filter upgrade.Filter) (aurUp, repoUp upgrade.UpSlice, err error) {
|
||||
filter upgrade.Filter,
|
||||
) (aurUp, repoUp upgrade.UpSlice, err error) {
|
||||
remote, remoteNames := query.GetRemotePackages(dbExecutor)
|
||||
|
||||
var (
|
||||
@ -125,7 +126,8 @@ func upList(ctx context.Context, warnings *query.AURWarnings, dbExecutor db.Exec
|
||||
}
|
||||
|
||||
func printLocalNewerThanAUR(
|
||||
remote []alpm.IPackage, aurdata map[string]*aur.Pkg) {
|
||||
remote []alpm.IPackage, aurdata map[string]*aur.Pkg,
|
||||
) {
|
||||
for _, pkg := range remote {
|
||||
aurPkg, ok := aurdata[pkg.Name()]
|
||||
if !ok {
|
||||
@ -134,7 +136,7 @@ func printLocalNewerThanAUR(
|
||||
|
||||
left, right := upgrade.GetVersionDiff(pkg.Version(), aurPkg.Version)
|
||||
|
||||
if !isDevelPackage(pkg) && alpm.VerCmp(pkg.Version(), aurPkg.Version) > 0 {
|
||||
if !isDevelPackage(pkg) && db.VerCmp(pkg.Version(), aurPkg.Version) > 0 {
|
||||
text.Warnln(gotext.Get("%s: local (%s) is newer than AUR (%s)",
|
||||
text.Cyan(pkg.Name()),
|
||||
left, right,
|
||||
@ -233,7 +235,8 @@ func upgradePkgsMenu(aurUp, repoUp upgrade.UpSlice) (stringset.StringSet, []stri
|
||||
|
||||
// Targets for sys upgrade.
|
||||
func sysupgradeTargets(ctx context.Context, dbExecutor db.Executor,
|
||||
enableDowngrade bool) (stringset.StringSet, []string, error) {
|
||||
enableDowngrade bool,
|
||||
) (stringset.StringSet, []string, error) {
|
||||
warnings := query.NewWarnings()
|
||||
|
||||
aurUp, repoUp, err := upList(ctx, warnings, dbExecutor, enableDowngrade,
|
||||
|
Loading…
Reference in New Issue
Block a user