mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-28 13:26:16 +03:00
first commit with the GitButler client. :)
This commit is contained in:
parent
fd6bdb24b3
commit
1707508b7e
@ -426,6 +426,29 @@ fn git_match_paths(
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn git_commit(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
message: &str,
|
||||
files: Vec<&str>,
|
||||
push: bool,
|
||||
) -> Result<bool, Error> {
|
||||
let app_state = handle.state::<App>();
|
||||
|
||||
let repo = repositories::Repository::open(
|
||||
&app_state.projects_storage,
|
||||
&app_state.users_storage,
|
||||
project_id,
|
||||
)?;
|
||||
|
||||
let success = repo
|
||||
.commit(message, files, push)
|
||||
.with_context(|| "Failed to commit")?;
|
||||
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let quit = tauri::CustomMenuItem::new("quit".to_string(), "Quit");
|
||||
let hide = tauri::CustomMenuItem::new("toggle".to_string(), format!("Hide {}", app_title()));
|
||||
@ -539,7 +562,8 @@ fn main() {
|
||||
search,
|
||||
git_status,
|
||||
git_file_paths,
|
||||
git_match_paths
|
||||
git_match_paths,
|
||||
git_commit
|
||||
]);
|
||||
|
||||
let tauri_context = generate_context!();
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{deltas, fs, projects, sessions, users};
|
||||
use anyhow::{Context, Result};
|
||||
use git2::Signature;
|
||||
use std::{collections::HashMap, path::Path};
|
||||
use tauri::regex::Regex;
|
||||
use walkdir::WalkDir;
|
||||
@ -157,8 +158,6 @@ impl Repository {
|
||||
|
||||
// get file status from git
|
||||
pub fn status(&self) -> Result<HashMap<String, String>> {
|
||||
println!("Git Status");
|
||||
|
||||
let mut options = git2::StatusOptions::new();
|
||||
options.include_untracked(true);
|
||||
options.include_ignored(false);
|
||||
@ -197,6 +196,51 @@ impl Repository {
|
||||
|
||||
return Ok(files);
|
||||
}
|
||||
|
||||
// commit method
|
||||
pub fn commit(&self, message: &str, files: Vec<&str>, push: bool) -> Result<bool> {
|
||||
println!("Git Commit");
|
||||
let repo = &self.git_repository;
|
||||
|
||||
let config = repo.config()?;
|
||||
let name = config.get_string("user.name")?;
|
||||
let email = config.get_string("user.email")?;
|
||||
|
||||
// Get the repository's index
|
||||
let mut index = repo.index()?;
|
||||
|
||||
// Add the specified files to the index
|
||||
for path_str in files {
|
||||
let path = Path::new(path_str);
|
||||
index.add_path(path)?;
|
||||
}
|
||||
|
||||
// Write the updated index to disk
|
||||
index.write()?;
|
||||
|
||||
// Get the default signature for the repository
|
||||
let signature = Signature::now(&name, &email)?;
|
||||
|
||||
// Create the commit with the updated index
|
||||
let tree_id = index.write_tree()?;
|
||||
let tree = repo.find_tree(tree_id)?;
|
||||
let parent_commit = repo.head()?.peel_to_commit()?;
|
||||
let commit = repo.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
message,
|
||||
&tree,
|
||||
&[&parent_commit],
|
||||
)?;
|
||||
println!("Created commit {}", commit);
|
||||
|
||||
if push {
|
||||
println!("Pushing to remote");
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
|
@ -5,20 +5,38 @@
|
||||
import BookmarkIcon from './icons/BookmarkIcon.svelte';
|
||||
import BranchIcon from './icons/BranchIcon.svelte';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { goto } from '$app/navigation';
|
||||
import { shortPath } from '$lib/paths';
|
||||
|
||||
let showCommand = false;
|
||||
let showCommit = false;
|
||||
|
||||
let is_command_down = false;
|
||||
let is_k_down = false;
|
||||
let is_c_down = false;
|
||||
|
||||
export let projectId: string;
|
||||
|
||||
let palette: HTMLElement;
|
||||
let commitPalette: HTMLElement;
|
||||
|
||||
let changedFiles = {};
|
||||
let commitMessage = '';
|
||||
let commitMessageInput: HTMLElement;
|
||||
|
||||
const listFiles = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_status', params);
|
||||
|
||||
const matchFiles = (params: { projectId: string; matchPattern: string }) =>
|
||||
invoke<Array<string>>('git_match_paths', params);
|
||||
|
||||
const commit = (params: {
|
||||
projectId: string;
|
||||
message: string;
|
||||
files: Array<string>;
|
||||
push: boolean;
|
||||
}) => invoke<boolean>('git_commit', params);
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.repeat) return;
|
||||
switch (event.key) {
|
||||
@ -29,8 +47,12 @@
|
||||
case 'k':
|
||||
is_k_down = true;
|
||||
break;
|
||||
case 'c':
|
||||
is_c_down = true;
|
||||
break;
|
||||
case 'Escape':
|
||||
showCommand = false;
|
||||
showCommit = false;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (showCommand) {
|
||||
@ -57,6 +79,10 @@
|
||||
document.getElementById('command')?.focus();
|
||||
}, 100);
|
||||
}
|
||||
if (is_command_down && is_c_down) {
|
||||
showCommit = true;
|
||||
executeCommand('commit');
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(event: KeyboardEvent) {
|
||||
@ -69,6 +95,10 @@
|
||||
is_k_down = false;
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'c':
|
||||
is_c_down = false;
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +107,9 @@
|
||||
if (showCommand && !palette.contains(target)) {
|
||||
showCommand = false;
|
||||
}
|
||||
if (showCommit && !commitPalette.contains(target)) {
|
||||
showCommit = false;
|
||||
}
|
||||
}
|
||||
|
||||
let activeClass = ['active', 'bg-zinc-700/50', 'text-white'];
|
||||
@ -121,10 +154,15 @@
|
||||
|
||||
function selectItem() {
|
||||
showCommand = false;
|
||||
showCommit = false;
|
||||
const menu = document.getElementById('commandMenu');
|
||||
if (menu) {
|
||||
const active = menu.querySelector('li.active');
|
||||
if (active) {
|
||||
const command = active.getAttribute('data-command');
|
||||
if (command) {
|
||||
executeCommand(command);
|
||||
}
|
||||
console.log('active', active);
|
||||
} else {
|
||||
goto('/projects/' + projectId + '/search?search=' + search);
|
||||
@ -132,6 +170,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
function executeCommand(command: string) {
|
||||
switch (command) {
|
||||
case 'commit':
|
||||
listFiles({ projectId: projectId }).then((files) => {
|
||||
console.log('files', files);
|
||||
changedFiles = files;
|
||||
});
|
||||
showCommit = true;
|
||||
setTimeout(function () {
|
||||
commitMessageInput.focus();
|
||||
}, 100);
|
||||
break;
|
||||
case 'bookmark':
|
||||
break;
|
||||
case 'branch':
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let search = '';
|
||||
|
||||
$: {
|
||||
@ -139,9 +196,9 @@
|
||||
}
|
||||
|
||||
let baseCommands = [
|
||||
{ text: 'Commit', key: 'C', icon: CommitIcon },
|
||||
{ text: 'Bookmark', key: 'B', icon: BookmarkIcon },
|
||||
{ text: 'Branch', key: 'H', icon: BranchIcon }
|
||||
{ text: 'Commit', key: 'C', icon: CommitIcon, command: 'commit' },
|
||||
{ text: 'Bookmark', key: 'B', icon: BookmarkIcon, command: 'bookmark' },
|
||||
{ text: 'Branch', key: 'H', icon: BranchIcon, command: 'branch' }
|
||||
];
|
||||
|
||||
$: menuItems = baseCommands;
|
||||
@ -171,12 +228,35 @@
|
||||
menuItems = searchResults;
|
||||
}
|
||||
}
|
||||
|
||||
function doCommit() {
|
||||
console.log('do commit', commitMessage);
|
||||
// get checked files
|
||||
let changedFiles: Array<string> = [];
|
||||
let doc = document.getElementsByClassName('file-checkbox');
|
||||
Array.from(doc).forEach((c) => {
|
||||
if (c.checked) {
|
||||
changedFiles.push(c.dataset['file']);
|
||||
}
|
||||
});
|
||||
console.log('files', changedFiles, commitMessage);
|
||||
commit({
|
||||
projectId: projectId,
|
||||
message: commitMessage,
|
||||
files: changedFiles,
|
||||
push: false
|
||||
}).then((result) => {
|
||||
console.log('commit result', result);
|
||||
commitMessage = '';
|
||||
showCommit = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} on:keyup={onKeyUp} on:click={checkCommandModal} />
|
||||
|
||||
<div>
|
||||
{#if showCommand}
|
||||
{#if showCommand || showCommit}
|
||||
<div class="relative z-10" role="dialog" aria-modal="true">
|
||||
<div
|
||||
class="fixed inset-0 bg-zinc-900 bg-opacity-80 transition-opacity"
|
||||
@ -184,69 +264,148 @@
|
||||
out:fade={{ duration: 50 }}
|
||||
/>
|
||||
|
||||
<div class="command-palette-modal fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<div
|
||||
bind:this={palette}
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="mx-auto max-w-2xl transform divide-y divide-zinc-500 divide-opacity-20 overflow-hidden rounded-xl bg-zinc-900 shadow-2xl transition-all border border-zinc-700"
|
||||
style="
|
||||
{#if showCommand}
|
||||
<div class="command-palette-modal fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<div
|
||||
bind:this={palette}
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="mx-auto max-w-2xl transform divide-y divide-zinc-500 divide-opacity-20 overflow-hidden rounded-xl bg-zinc-900 shadow-2xl transition-all border border-zinc-700"
|
||||
style="
|
||||
border-width: 0.5px;
|
||||
-webkit-backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%);
|
||||
backdrop-filter: blur(20px) saturate(190%) contrast(70%) brightness(80%);
|
||||
background-color: rgba(24, 24, 27, 0.60);
|
||||
border: 0.5px solid rgba(63, 63, 70, 0.50);"
|
||||
>
|
||||
<div class="relative">
|
||||
<svg
|
||||
class="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-zinc-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="command"
|
||||
type="text"
|
||||
bind:value={search}
|
||||
class="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-white focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Default state, show/hide based on command palette state. -->
|
||||
<ul
|
||||
class="max-h-80 scroll-py-2 divide-y divide-zinc-500 divide-opacity-20 overflow-y-auto"
|
||||
>
|
||||
<li class="p-1">
|
||||
<ul id="commandMenu" class="text-sm text-zinc-400">
|
||||
{#each menuItems as item}
|
||||
<!-- Active: "bg-zinc-800 text-white" -->
|
||||
<li
|
||||
class="group flex cursor-default select-none items-center rounded-md px-3 py-2"
|
||||
>
|
||||
<!-- Active: "text-white", Not Active: "text-zinc-500" -->
|
||||
<svelte:component this={item.icon} />
|
||||
<span class="ml-3 flex-auto truncate">{item.text}</span>
|
||||
{#if item.key}
|
||||
<span class="ml-3 flex-none text-xs font-semibold text-zinc-400 px-1 py-1 bg-zinc-800 border-b border-black rounded">
|
||||
<kbd class="font-sans">⌘</kbd><kbd class="font-sans">{item.key}</kbd>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="relative">
|
||||
<svg
|
||||
class="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-zinc-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
id="command"
|
||||
type="text"
|
||||
bind:value={search}
|
||||
class="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-white focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Default state, show/hide based on command palette state. -->
|
||||
<ul
|
||||
class="max-h-80 scroll-py-2 divide-y divide-zinc-500 divide-opacity-20 overflow-y-auto"
|
||||
>
|
||||
<li class="p-1">
|
||||
<ul id="commandMenu" class="text-sm text-zinc-400">
|
||||
{#each menuItems as item}
|
||||
<!-- Active: "bg-zinc-800 text-white" -->
|
||||
<li
|
||||
class="group flex cursor-default select-none items-center rounded-md px-3 py-2"
|
||||
on:click={() => {
|
||||
executeCommand(item.command);
|
||||
}}
|
||||
data-command={item.command}
|
||||
>
|
||||
<!-- Active: "text-white", Not Active: "text-zinc-500" -->
|
||||
<svelte:component this={item.icon} />
|
||||
<span class="ml-3 flex-auto truncate">{item.text}</span>
|
||||
{#if item.key}
|
||||
<span
|
||||
class="ml-3 flex-none text-xs font-semibold text-zinc-400 px-1 py-1 bg-zinc-800 border-b border-black rounded"
|
||||
>
|
||||
<kbd class="font-sans">⌘</kbd><kbd class="font-sans">{item.key}</kbd>
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showCommit}
|
||||
<div class="commit-palette-modal fixed inset-0 z-10 overflow-y-auto p-4 sm:p-6 md:p-20">
|
||||
<div
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
bind:this={commitPalette}
|
||||
class="mx-auto max-w-2xl transform overflow-hidden rounded-xl bg-zinc-900 shadow-2xl transition-all border border-zinc-700"
|
||||
style="
|
||||
border-width: 0.5px;
|
||||
border: 0.5px solid rgba(63, 63, 70, 0.50);"
|
||||
>
|
||||
<div class="w-full border-b border-zinc-700 text-lg text-white mb-4 p-4">
|
||||
Commit Your Changes
|
||||
</div>
|
||||
<div
|
||||
class="relative transform overflow-hidden text-left transition-all sm:w-full sm:max-w-sm p-2 m-auto"
|
||||
>
|
||||
{#if Object.entries(changedFiles).length > 0}
|
||||
<div>
|
||||
<div class="">
|
||||
<h3 class="text-base font-semibold text-zinc-200" id="modal-title">
|
||||
Commit Message
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<div class="mt-2">
|
||||
<textarea
|
||||
rows="4"
|
||||
name="message"
|
||||
id="commit-message"
|
||||
bind:this={commitMessageInput}
|
||||
bind:value={commitMessage}
|
||||
class="block w-full rounded-md p-4 border-0 text-zinc-200 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-6">
|
||||
<button
|
||||
type="button"
|
||||
on:click={doCommit}
|
||||
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>Commit Your Changes</button
|
||||
>
|
||||
</div>
|
||||
<div class="text-zinc-200 mt-4 py-4">
|
||||
<h3 class="text-base font-semibold text-zinc-200" id="modal-title">
|
||||
Changed Files
|
||||
</h3>
|
||||
{#each Object.entries(changedFiles) as file}
|
||||
<div class="flex flex-row space-x-2">
|
||||
<div>
|
||||
<input type="checkbox" class="file-checkbox" data-file={file[0]} checked />
|
||||
</div>
|
||||
<div>
|
||||
{file[1]}
|
||||
</div>
|
||||
<div class="font-mono">
|
||||
{shortPath(file[0])}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-white mx-auto text-center">No changes to commit</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
12
src/lib/paths.ts
Normal file
12
src/lib/paths.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export function shortPath(path: string, max = 3) {
|
||||
if (path.length < 30) {
|
||||
return path;
|
||||
}
|
||||
const pathParts = path.split('/');
|
||||
const file = pathParts.pop();
|
||||
if (pathParts.length > 0) {
|
||||
const pp = pathParts.map((p) => p.slice(0, max)).join('/');
|
||||
return `${pp}/${file}`;
|
||||
}
|
||||
return file;
|
||||
}
|
@ -4,25 +4,13 @@
|
||||
import type { UISession } from '$lib/uisessions';
|
||||
import type { Activity } from '$lib/sessions';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import { shortPath } from '$lib/paths';
|
||||
|
||||
export let data: LayoutData;
|
||||
$: project = data.project;
|
||||
$: dateSessions = data.dateSessions as Readable<Record<number, UISession[]>>;
|
||||
$: filesStatus = data.filesStatus;
|
||||
|
||||
function shortPath(path: string, max = 3) {
|
||||
if (path.length < 30) {
|
||||
return path;
|
||||
}
|
||||
const pathParts = path.split('/');
|
||||
const file = pathParts.pop();
|
||||
if (pathParts.length > 0) {
|
||||
const pp = pathParts.map((p) => p.slice(0, max)).join('/');
|
||||
return `${pp}/${file}`;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
// convert a list of timestamps to a sparkline
|
||||
function timestampsToSpark(tsArray: number[]) {
|
||||
let range = tsArray[0] - tsArray[tsArray.length - 1];
|
||||
@ -147,7 +135,9 @@
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
<div class="results-card rounded bg-[#2F2F33] border border-zinc-700 p-4 drop-shadow-lg">
|
||||
<div
|
||||
class="results-card rounded bg-[#2F2F33] border border-zinc-700 p-4 drop-shadow-lg"
|
||||
>
|
||||
{#each Object.entries(fileSessions) as filetime}
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="font-mono text-zinc-100">{filetime[0]}</div>
|
||||
@ -171,9 +161,17 @@
|
||||
<div class="work-in-progress-container border-b border-zinc-700 py-4 px-4">
|
||||
<h2 class="mb-2 text-lg font-bold text-zinc-300">Work in Progress</h2>
|
||||
{#if $filesStatus.length == 0}
|
||||
<div class="flex align-middle rounded border border-green-700 bg-green-900 p-4 text-green-400">
|
||||
<div
|
||||
class="flex align-middle rounded border border-green-700 bg-green-900 p-4 text-green-400"
|
||||
>
|
||||
<div class="icon h-5 w-5 mr-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#4ADE80" fill-rule="evenodd" d="M2 10a8 8 0 1 0 16 0 8 8 0 0 0-16 0Zm12.16-1.44a.8.8 0 0 0-1.12-1.12L9.2 11.28 7.36 9.44a.8.8 0 0 0-1.12 1.12l2.4 2.4c.32.32.8.32 1.12 0l4.4-4.4Z" /></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||
><path
|
||||
fill="#4ADE80"
|
||||
fill-rule="evenodd"
|
||||
d="M2 10a8 8 0 1 0 16 0 8 8 0 0 0-16 0Zm12.16-1.44a.8.8 0 0 0-1.12-1.12L9.2 11.28 7.36 9.44a.8.8 0 0 0-1.12 1.12l2.4 2.4c.32.32.8.32 1.12 0l4.4-4.4Z"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
Everything is committed
|
||||
</div>
|
||||
@ -196,7 +194,9 @@
|
||||
>
|
||||
<h2 class="text-lg font-bold text-zinc-300">Recent Activity</h2>
|
||||
{#each recentActivity($dateSessions) as activity}
|
||||
<div class="recent-activity-card mt-4 mb-1 rounded border border-zinc-700 text-zinc-400 drop-shadow-lg">
|
||||
<div
|
||||
class="recent-activity-card mt-4 mb-1 rounded border border-zinc-700 text-zinc-400 drop-shadow-lg"
|
||||
>
|
||||
<div class="flex flex-col p-3 rounded bg-[#2F2F33]">
|
||||
<div class="flex flex-row justify-between text-zinc-500 pb-2">
|
||||
<div class="">
|
||||
|
Loading…
Reference in New Issue
Block a user