Téléverser les fichiers vers "sk1"
This commit is contained in:
parent
08b902f8fe
commit
f366894638
12
sk1/Dockerfile
Normal file
12
sk1/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Dockerfile
|
||||||
|
FROM php:8.2-apache
|
||||||
|
|
||||||
|
# 1) Update packages and install libpq-dev (libpq-fe.h)
|
||||||
|
# 2) Install pdo_pgsql
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends libpq-dev \
|
||||||
|
&& docker-php-ext-install pdo_pgsql \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 3) Copy your application into the web directory
|
||||||
|
COPY src/ /var/www/html/
|
199
sk1/README.md
Normal file
199
sk1/README.md
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
# PHP + PostgreSQL Visit Counter
|
||||||
|
|
||||||
|
This project implements a PHP web application for counting visits, backed by a PostgreSQL database. It is containerized using Docker and Docker Compose, and provides automated deployment scripts (to Plesk via Git) as well as deployment cleanup scripts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [Features](#features)
|
||||||
|
3. [Prerequisites](#prerequisites)
|
||||||
|
4. [Installation & Configuration](#installation--configuration)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [Docker and Docker Compose](#docker-and-docker-compose)
|
||||||
|
- [Prepare Script (`prepare-app.sh`)](#prepare-script-prepare-appsh)
|
||||||
|
5. [Usage](#usage)
|
||||||
|
- [Running Locally](#running-locally)
|
||||||
|
- [Application Entry Point](#application-entry-point)
|
||||||
|
- [Table Reset (`drop.php`)](#table-reset-dropphp)
|
||||||
|
6. [Deployment to Plesk](#deployment-to-plesk)
|
||||||
|
7. [Deployment Cleanup (`remove-app.sh`)](#deployment-cleanup-remove-appsh)
|
||||||
|
8. [Detailed File Structure](#detailed-file-structure)
|
||||||
|
9. [Contributing](#contributing)
|
||||||
|
10. [Troubleshooting](#troubleshooting)
|
||||||
|
11. [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This simple application counts the number of visits to a web page. On each page load, a new entry is inserted into the `visitors` table in PostgreSQL, and the total count is displayed.
|
||||||
|
|
||||||
|
A button allows to choose between light and dark themes via JavaScript and `localStorage`.
|
||||||
|
|
||||||
|
The project includes:
|
||||||
|
|
||||||
|
- A **PostgreSQL** container with automatic table initialization via `db.sql`.
|
||||||
|
- A **PHP 8.2 + Apache** container serving the application.
|
||||||
|
- A **Dockerfile** to build the web image (with the `pdo_pgsql` extension installed).
|
||||||
|
- A **docker-compose.yml** to orchestrate both services.
|
||||||
|
- A **prepare-app.sh** script for force-pushing deployments to Plesk via Git.
|
||||||
|
- A **remove-app.sh** script for cleaning up the remote deployment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Automatically inserts a row into the `visitors` table on each load (`INSERT INTO visitors DEFAULT VALUES`).
|
||||||
|
- Automatically creates the table if it doesn’t exist (`CREATE TABLE IF NOT EXISTS visitors …`).
|
||||||
|
- Displays the visit counter.
|
||||||
|
- Light/dark theme toggle, persisted in `localStorage`.
|
||||||
|
- Automated Git deployments (Plesk).
|
||||||
|
- Remote cleanup of pushed content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Docker** (>= 20.x)
|
||||||
|
- **Docker Compose** (>= 1.27.x)
|
||||||
|
- **Git** (for the deployment scripts)
|
||||||
|
- Access to a Plesk instance with a Git-configured repository (for `prepare-app.sh` and `remove-app.sh`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation & Configuration
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
├── db.sql # SQL script to initialize the `visitors` table
|
||||||
|
├── Dockerfile # Builds the PHP + Apache + pdo_pgsql image
|
||||||
|
├── docker-compose.yml # Docker orchestration (db + web)
|
||||||
|
├── prepare-app.sh # Deployment script to Plesk
|
||||||
|
├── remove-app.sh # Remote cleanup script
|
||||||
|
├── index.php # Main page (insertion + count + front-end)
|
||||||
|
└── drop.php # Script to drop the `visitors` table
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
In `docker-compose.yml`, for the **db** service:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
POSTGRES_DB: visitors
|
||||||
|
POSTGRES_USER: visitor_user
|
||||||
|
POSTGRES_PASSWORD: secret
|
||||||
|
```
|
||||||
|
|
||||||
|
And for the **web** service:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: visitors
|
||||||
|
DB_USER: visitor_user
|
||||||
|
DB_PASS: secret
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Prepare Script (`prepare-app.sh`)
|
||||||
|
|
||||||
|
This script initializes the Git repo (if needed), adds a `tuke` remote pointing to the Plesk instance, commits all changes, then force-pushes the branch (`master` or `main`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x prepare-app.sh
|
||||||
|
./prepare-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
|
||||||
|
### Application Entry Point
|
||||||
|
|
||||||
|
`index.php` performs:
|
||||||
|
|
||||||
|
1. Connects to PostgreSQL via PDO (`pgsql:host=$dbHost;port=$dbPort;dbname=$dbName`).
|
||||||
|
2. Runs an embedded SQL migration to create the `visitors` table if it doesn’t exist.
|
||||||
|
3. Inserts a new row:
|
||||||
|
```php
|
||||||
|
$pdo->exec("INSERT INTO visitors DEFAULT VALUES");
|
||||||
|
```
|
||||||
|
4. Retrieves the total count:
|
||||||
|
```php
|
||||||
|
$count = $pdo->query("SELECT COUNT(*) FROM visitors")->fetchColumn();
|
||||||
|
```
|
||||||
|
5. Generates the HTML/CSS/JS to display the counter and manage the theme.
|
||||||
|
|
||||||
|
### Table Reset (`drop.php`)
|
||||||
|
|
||||||
|
The script `drop.php` connects to the same database and runs:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$pdo->exec("DROP TABLE IF EXISTS visitors");
|
||||||
|
echo "Table visitors dropped.";
|
||||||
|
```
|
||||||
|
|
||||||
|
On error it returns HTTP 500 and displays the exception message.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment to Plesk
|
||||||
|
|
||||||
|
To deploy automatically:
|
||||||
|
|
||||||
|
1. Configure `GIT_REMOTE_URL` and `BRANCH` in `prepare-app.sh`.
|
||||||
|
2. Make it executable and run:
|
||||||
|
```bash
|
||||||
|
chmod +x prepare-app.sh
|
||||||
|
./prepare-app.sh
|
||||||
|
```
|
||||||
|
This force-pushes the branch to Plesk’s `httpdocs`, updating the remote site.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Cleanup (`remove-app.sh`)
|
||||||
|
|
||||||
|
To clear the remote content without touching the local repository:
|
||||||
|
|
||||||
|
1. Make it executable:
|
||||||
|
```bash
|
||||||
|
chmod +x remove-app.sh
|
||||||
|
```
|
||||||
|
2. Run:
|
||||||
|
```bash
|
||||||
|
./remove-app.sh
|
||||||
|
```
|
||||||
|
This clones the remote into a temp directory, removes all tracked files, commits, and force-pushes, then cleans up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed File Structure
|
||||||
|
|
||||||
|
- **docker-compose.yml**: Coordinates two services:
|
||||||
|
- **db** (PostgreSQL 13) initialized via `./db.sql`.
|
||||||
|
- **web** (PHP 8.2 + Apache with pdo_pgsql) linked to `db`, exposing port 8080.
|
||||||
|
- **Dockerfile**: Base `php:8.2-apache`, installs `libpq-dev` and `pdo_pgsql`, copies `src/` to `/var/www/html/`.
|
||||||
|
- **db.sql**: SQL to auto-create the `visitors` table.
|
||||||
|
- **prepare-app.sh** & **remove-app.sh**: Bash scripts for Git-based deployment and cleanup on Plesk.
|
||||||
|
- **index.php**: Main page logic (connect, migrate, insert, count, render).
|
||||||
|
- **drop.php**: Drops the `visitors` table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **PostgreSQL connection error**: Ensure the `db` service is running; check logs:
|
||||||
|
```bash
|
||||||
|
docker-compose logs db
|
||||||
|
```
|
||||||
|
- **Table not created**: Make sure `db.sql` is mounted under `/docker-entrypoint-initdb.d/` and that the `db_data` volume is empty (initialization only runs on a fresh volume).
|
||||||
|
- **Git push rejected**: Verify remote URL and credentials in `prepare-app.sh`.
|
||||||
|
|
4
sk1/db.sql
Normal file
4
sk1/db.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS visitors (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
|
);
|
27
sk1/docker-compose.yml
Normal file
27
sk1/docker-compose.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:13
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: visitors
|
||||||
|
POSTGRES_USER: visitor_user
|
||||||
|
POSTGRES_PASSWORD: secret
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
- ./db.sql:/docker-entrypoint-initdb.d/db.sql # ← auto‑run migration
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: visitors
|
||||||
|
DB_USER: visitor_user
|
||||||
|
DB_PASS: secret
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
20
sk1/drop.php
Normal file
20
sk1/drop.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
// ─── DB CONFIG ─────────────────────────────────────────────────
|
||||||
|
$dbHost = 'localhost';
|
||||||
|
$dbPort = '5432';
|
||||||
|
$dbName = 'af585fp_db';
|
||||||
|
$dbUser = 'database_user';
|
||||||
|
$dbPass = 'password_database';
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$dsn = "pgsql:host=$dbHost;port=$dbPort;dbname=$dbName";
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, $dbUser, $dbPass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||||
|
]);
|
||||||
|
$pdo->exec("DROP TABLE IF EXISTS visitors");
|
||||||
|
echo "Table 'visitors' dropped.";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Deletion error: " . $e->getMessage();
|
||||||
|
}
|
153
sk1/index.php
Normal file
153
sk1/index.php
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
// ─── DB CONFIG ─────────────────────────────────────────────────
|
||||||
|
$dbHost = 'localhost';
|
||||||
|
$dbPort = '5432';
|
||||||
|
$dbName = 'af585fp_db';
|
||||||
|
$dbUser = 'database_user';
|
||||||
|
$dbPass = 'password_database';
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
$dsn = "pgsql:host=$dbHost;port=$dbPort;dbname=$dbName";
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, $dbUser, $dbPass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||||
|
]);
|
||||||
|
// Auto-migration: if the table does not exist, create it
|
||||||
|
$pdo->exec(<<<'SQL'
|
||||||
|
CREATE TABLE IF NOT EXISTS visitors (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
visited_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("Database connection or migration failed: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a row then count
|
||||||
|
$pdo->exec("INSERT INTO visitors DEFAULT VALUES");
|
||||||
|
$count = $pdo->query("SELECT COUNT(*) FROM visitors")->fetchColumn();
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Visit Counter</title>
|
||||||
|
<style>
|
||||||
|
/* Import Google Fonts */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-light: #f0f4f8;
|
||||||
|
--bg-dark: #1a1a2e;
|
||||||
|
--card-bg-light: #ffffff;
|
||||||
|
--card-bg-dark: rgba(26,26,46,0.8);
|
||||||
|
--primary: #2e86de;
|
||||||
|
--text-light: #333333;
|
||||||
|
--text-dark: #eaeaea;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--bg-light);
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
body.dark {
|
||||||
|
background: var(--bg-dark);
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg-light);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
|
||||||
|
text-align: center;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 400px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
transition: background 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
body.dark .card {
|
||||||
|
background: var(--card-bg-dark);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
background: var(--primary);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
.controls button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div class="controls">
|
||||||
|
<button id="refreshBtn">↻ Refresh</button>
|
||||||
|
<button id="themeToggle">🌙</button>
|
||||||
|
</div>
|
||||||
|
<h1>Visit Counter</h1>
|
||||||
|
<p>You are visitor # <strong><?= htmlspecialchars($count) ?></strong></p>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Refresh the page without submitting the count twice
|
||||||
|
document.getElementById('refreshBtn').addEventListener('click', () => {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
// Handling light/dark theme
|
||||||
|
const toggle = document.getElementById('themeToggle');
|
||||||
|
const applyTheme = (dark) => {
|
||||||
|
document.body.classList.toggle('dark', dark);
|
||||||
|
toggle.textContent = dark ? '☀️' : '🌙';
|
||||||
|
localStorage.setItem('darkMode', dark);
|
||||||
|
};
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
applyTheme(!document.body.classList.contains('dark'));
|
||||||
|
});
|
||||||
|
// Initialization
|
||||||
|
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||||
|
applyTheme(darkMode);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
36
sk1/prepare-app.sh
Normal file
36
sk1/prepare-app.sh
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ─── GIT CONFIGURATION ───────────────────────────────────────────────
|
||||||
|
GIT_REMOTE_URL="https://af585fp_ftp:P32ubNLJPhnX_ftp@antonin.filippi.website.tuke.sk/plesk-git/exam-app"
|
||||||
|
BRANCH="master" # or "main" if that’s your default branch
|
||||||
|
# ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
command -v git >/dev/null 2>&1 || { echo "Install git"; exit 1; }
|
||||||
|
|
||||||
|
echo "→ Initializing repository (if necessary)…"
|
||||||
|
if [ ! -d .git ]; then
|
||||||
|
git init
|
||||||
|
git checkout -b $BRANCH
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ Ensuring you are on branch $BRANCH…"
|
||||||
|
git checkout $BRANCH
|
||||||
|
|
||||||
|
echo "→ Configuring remote 'tuke'…"
|
||||||
|
git remote remove tuke 2>/dev/null || true
|
||||||
|
git remote add tuke "$GIT_REMOTE_URL"
|
||||||
|
|
||||||
|
echo "→ Refreshing the index…"
|
||||||
|
# Add all files, including deleted ones
|
||||||
|
git add -A
|
||||||
|
|
||||||
|
echo "→ Committing changes if necessary…"
|
||||||
|
# The || true prevents an error if no changes are detected
|
||||||
|
git commit -m "Automatic deployment $(date '+%Y-%m-%d %H:%M:%S')" || echo "→ No changes to commit"
|
||||||
|
|
||||||
|
echo "→ Force pushing to Plesk ($BRANCH)…"
|
||||||
|
git push --force tuke $BRANCH:$BRANCH
|
||||||
|
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo "→ Open: https://antonin.filippi.website.tuke.sk"
|
29
sk1/remove-app.sh
Normal file
29
sk1/remove-app.sh
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ─── CONFIGURATION ─────────────────────────────────────────────────
|
||||||
|
GIT_REMOTE_URL="https://af585fp_ftp:P32ubNLJPhnX_ftp@antonin.filippi.website.tuke.sk/plesk-git/exam-app"
|
||||||
|
BRANCH="master" # or "main" depending on your repo
|
||||||
|
# ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Prerequisites
|
||||||
|
command -v git >/dev/null 2>&1 || { echo "Error: git not found"; exit 1; }
|
||||||
|
|
||||||
|
# 1) Create a temporary directory
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
echo "→ Cloning into $TMPDIR…"
|
||||||
|
git clone --branch "$BRANCH" --single-branch "$GIT_REMOTE_URL" "$TMPDIR"
|
||||||
|
|
||||||
|
# 2) In this clone, remove all tracked files
|
||||||
|
cd "$TMPDIR"
|
||||||
|
git rm -r *
|
||||||
|
|
||||||
|
# 3) Commit and force push
|
||||||
|
git commit -m "Cleanup remote httpdocs"
|
||||||
|
git push --force origin "$BRANCH":"$BRANCH"
|
||||||
|
|
||||||
|
# 4) Cleanup
|
||||||
|
cd -
|
||||||
|
rm -rf "$TMPDIR"
|
||||||
|
|
||||||
|
echo "✅ Remote cleared. Your local repository has not been modified."
|
Loading…
Reference in New Issue
Block a user