mirror of
https://github.com/mike-rambil/Advanced-Git.git
synced 2025-12-20 09:00:43 -06:00
ci: use issure form template to submit (#72)
Co-authored-by: Really Him <hesereallyhim@proton.me>
This commit is contained in:
233
.github/ISSUE_TEMPLATE/command-submission.yml
vendored
Normal file
233
.github/ISSUE_TEMPLATE/command-submission.yml
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
name: "Git Command Submission"
|
||||
description: "Submit a new Git command entry using the required format from dev-docs/FORMAT.md."
|
||||
title: "Add: <command name>"
|
||||
labels:
|
||||
- "new command"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Contribution Format Guide
|
||||
|
||||
**Purpose:** This template captures the required format for documenting Git commands in this repository. All contributors should follow this structure when adding or updating entries in `toc-source.json`.
|
||||
|
||||
**How to Contribute:**
|
||||
|
||||
- When you want to add a new Git command or script, copy the template below and fill in each field as described.
|
||||
- Place your new entry in `toc-source.json` using this format.
|
||||
- Submit your changes via a Pull Request (PR).
|
||||
- Well-formatted contributions make it easy to generate documentation and keep the project organized.
|
||||
|
||||
**Where to Find This:**
|
||||
|
||||
- This template reflects `dev-docs/FORMAT.md` (always up to date).
|
||||
- Main documentation and contribution guidelines may also reference this file.
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: "Name"
|
||||
placeholder: "git example-command"
|
||||
description: |
|
||||
Type: string
|
||||
Description: The official name/title of the command or script.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: category
|
||||
attributes:
|
||||
label: "Category"
|
||||
placeholder: "History"
|
||||
description: |
|
||||
Type: string
|
||||
Description: The category this command belongs to (e.g., Collaboration, History, Worktree).
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: short_description
|
||||
attributes:
|
||||
label: "Short Description"
|
||||
placeholder: "Summarize what the command does."
|
||||
description: |
|
||||
Type: string
|
||||
Description: A concise summary of what the command does.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: long_description
|
||||
attributes:
|
||||
label: "Long Description"
|
||||
description: |
|
||||
Type: string (optional)
|
||||
Description: A more detailed explanation of the command, its context, and use cases.
|
||||
Please provide plain text or Markdown.
|
||||
placeholder: "Explain when and why to use this command..."
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: command
|
||||
attributes:
|
||||
label: "Command"
|
||||
description: |
|
||||
Type: string (optional)
|
||||
Description: The full command syntax, including placeholders for arguments.
|
||||
placeholder: "git example-command <arguments>"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: "Examples"
|
||||
description: |
|
||||
Type: list of objects
|
||||
Each object:
|
||||
- `code`: string (the command as used)
|
||||
- `description`: string (what the example does)
|
||||
Provide as valid JSON. Example:
|
||||
[
|
||||
{
|
||||
"code": "git clean -n -d",
|
||||
"description": "Preview what will be deleted (dry run)."
|
||||
}
|
||||
]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: "Steps"
|
||||
description: |
|
||||
Type: list of strings
|
||||
Description: Step-by-step instructions for using the command.
|
||||
Provide as a JSON array of strings.
|
||||
placeholder: |
|
||||
[
|
||||
"First step...",
|
||||
"Second step..."
|
||||
]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: flags
|
||||
attributes:
|
||||
label: "Flags"
|
||||
description: |
|
||||
Type: object (optional)
|
||||
Each key: flag (e.g., `-f`, `-d`)
|
||||
Each value: string (description of the flag)
|
||||
Provide as a JSON object.
|
||||
placeholder: |
|
||||
{
|
||||
"-f": "Forces the action."
|
||||
}
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: prerequisites
|
||||
attributes:
|
||||
label: "Prerequisites"
|
||||
description: |
|
||||
Type: list of strings (optional)
|
||||
Description: Requirements before using the command.
|
||||
Provide as a JSON array of strings.
|
||||
placeholder: |
|
||||
[
|
||||
"Have a clean working tree."
|
||||
]
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: warnings
|
||||
attributes:
|
||||
label: "Warnings"
|
||||
description: |
|
||||
Type: list of strings (optional)
|
||||
Description: Important cautions or risks.
|
||||
Provide as a JSON array of strings.
|
||||
placeholder: |
|
||||
[
|
||||
"This will permanently delete files."
|
||||
]
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: protips
|
||||
attributes:
|
||||
label: "ProTips"
|
||||
description: |
|
||||
Type: list of strings (optional)
|
||||
Description: Short, actionable tips to help users be more successful with this command. Use 1–2 lines per tip.
|
||||
Provide as a JSON array of strings.
|
||||
placeholder: |
|
||||
[
|
||||
"Use `-n` for a dry run before deleting files."
|
||||
]
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: tags
|
||||
attributes:
|
||||
label: "Tags"
|
||||
description: |
|
||||
Type: list of strings
|
||||
Description: Keywords for search and categorization.
|
||||
Provide as a JSON array of strings.
|
||||
placeholder: |
|
||||
[
|
||||
"clean",
|
||||
"workspace"
|
||||
]
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: author
|
||||
attributes:
|
||||
label: "Author"
|
||||
description: |
|
||||
Type: string
|
||||
Description: Who contributed this entry.
|
||||
placeholder: "your-github-handle"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: last_updated
|
||||
attributes:
|
||||
label: "Last Updated"
|
||||
description: |
|
||||
Type: string (date)
|
||||
Description: Last update date (YYYY-MM-DD).
|
||||
placeholder: "2025-10-14"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: links
|
||||
attributes:
|
||||
label: "Links"
|
||||
description: |
|
||||
Type: list of objects (optional)
|
||||
Each object:
|
||||
- `label`: string (e.g., \"Official Docs\")
|
||||
- `url`: string (link to resource)
|
||||
Provide as valid JSON.
|
||||
placeholder: |
|
||||
[
|
||||
{
|
||||
"label": "Official Docs",
|
||||
"url": "https://git-scm.com/docs/git-clean"
|
||||
}
|
||||
]
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: related_commands
|
||||
attributes:
|
||||
label: "Related Commands"
|
||||
description: |
|
||||
Type: list of strings (optional)
|
||||
Description: Other commands related to this one.
|
||||
Provide as a JSON array of strings.
|
||||
placeholder: |
|
||||
[
|
||||
"git status"
|
||||
]
|
||||
validations:
|
||||
required: false
|
||||
38
.github/workflows/issue-to-toc.yml
vendored
Normal file
38
.github/workflows/issue-to-toc.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: "Convert Issue Submissions to TOC PR"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ contains(join(github.event.issue.labels.*.name, ','), 'new command') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Generate toc entry from issue
|
||||
run: node scripts/issue-to-toc.js
|
||||
env:
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: issue/${{ github.event.issue.number }}-toc-entry
|
||||
delete-branch: true
|
||||
commit-message: Add command from issue #${{ github.event.issue.number }}
|
||||
title: Add command from issue #${{ github.event.issue.number }}
|
||||
body: |
|
||||
## Summary
|
||||
- Generated from issue #${{ github.event.issue.number }} using the Git command submission template.
|
||||
- Adds a new entry to `toc-source.json`.
|
||||
301
scripts/issue-to-toc.js
Normal file
301
scripts/issue-to-toc.js
Normal file
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ISSUE_BODY = process.env.ISSUE_BODY;
|
||||
const ISSUE_NUMBER = process.env.ISSUE_NUMBER || "unknown";
|
||||
const WORKSPACE = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||
|
||||
if (!ISSUE_BODY) {
|
||||
console.error("ISSUE_BODY environment variable is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const FIELD_CONFIG = [
|
||||
{ heading: "Name", key: "Name", required: true, parser: asString },
|
||||
{ heading: "Category", key: "category", required: true, parser: asString },
|
||||
{
|
||||
heading: "Short Description",
|
||||
key: "short_description",
|
||||
required: true,
|
||||
parser: asString,
|
||||
},
|
||||
{
|
||||
heading: "Long Description",
|
||||
key: "long_description",
|
||||
required: false,
|
||||
parser: asOptionalString,
|
||||
},
|
||||
{ heading: "Command", key: "command", required: false, parser: asOptionalString },
|
||||
{
|
||||
heading: "Examples",
|
||||
key: "examples",
|
||||
required: true,
|
||||
parser: asExamples,
|
||||
},
|
||||
{
|
||||
heading: "Steps",
|
||||
key: "steps",
|
||||
required: true,
|
||||
parser: asStringArray,
|
||||
},
|
||||
{ heading: "Flags", key: "flags", required: false, parser: asFlags },
|
||||
{
|
||||
heading: "Prerequisites",
|
||||
key: "prerequisites",
|
||||
required: false,
|
||||
parser: asOptionalStringArray,
|
||||
},
|
||||
{
|
||||
heading: "Warnings",
|
||||
key: "warnings",
|
||||
required: false,
|
||||
parser: asOptionalStringArray,
|
||||
},
|
||||
{
|
||||
heading: "ProTips",
|
||||
key: "protips",
|
||||
required: false,
|
||||
parser: asOptionalStringArray,
|
||||
},
|
||||
{
|
||||
heading: "Tags",
|
||||
key: "tags",
|
||||
required: true,
|
||||
parser: asStringArray,
|
||||
},
|
||||
{ heading: "Author", key: "author", required: true, parser: asString },
|
||||
{
|
||||
heading: "Last Updated",
|
||||
key: "last_updated",
|
||||
required: true,
|
||||
parser: asDateString,
|
||||
},
|
||||
{ heading: "Links", key: "links", required: false, parser: asLinks },
|
||||
{
|
||||
heading: "Related Commands",
|
||||
key: "related_commands",
|
||||
required: false,
|
||||
parser: asOptionalStringArray,
|
||||
},
|
||||
];
|
||||
|
||||
const sections = parseSections(ISSUE_BODY);
|
||||
const entry = {};
|
||||
|
||||
for (const field of FIELD_CONFIG) {
|
||||
const rawValue = sections[field.heading] ?? "";
|
||||
if (!rawValue && field.required) {
|
||||
fail(`Field "${field.heading}" is required but was not provided.`);
|
||||
}
|
||||
const parsedValue = field.parser(rawValue, field.heading);
|
||||
if (parsedValue !== undefined) {
|
||||
entry[field.key] = parsedValue;
|
||||
}
|
||||
}
|
||||
|
||||
const tocPath = path.join(WORKSPACE, "toc-source.json");
|
||||
const toc = readJson(tocPath);
|
||||
ensureUniqueName(entry.Name, toc);
|
||||
toc.push(entry);
|
||||
writeJson(tocPath, toc);
|
||||
|
||||
console.log(`Entry for "${entry.Name}" added to toc-source.json from issue #${ISSUE_NUMBER}.`);
|
||||
|
||||
function parseSections(body) {
|
||||
const normalized = body.replace(/\r\n/g, "\n").trim();
|
||||
const regex = /### ([^\n]+)\n([\s\S]*?)(?=(?:\n### [^\n]+)|$)/g;
|
||||
const result = {};
|
||||
let match;
|
||||
while ((match = regex.exec(normalized)) !== null) {
|
||||
const heading = match[1].trim();
|
||||
const value = match[2] ?? "";
|
||||
result[heading] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function cleanupValue(value) {
|
||||
if (!value) return "";
|
||||
let cleaned = value.trim();
|
||||
const isNoResponse = (text) =>
|
||||
text.replace(/\s+/g, " ").toLowerCase() === "_no response_";
|
||||
if (isNoResponse(cleaned)) {
|
||||
return "";
|
||||
}
|
||||
if (cleaned.startsWith("```")) {
|
||||
const lines = cleaned.split(/\r?\n/);
|
||||
if (lines[0].startsWith("```")) {
|
||||
lines.shift();
|
||||
const last = lines[lines.length - 1];
|
||||
if (last && last.startsWith("```")) {
|
||||
lines.pop();
|
||||
}
|
||||
cleaned = lines.join("\n").trim();
|
||||
if (isNoResponse(cleaned)) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
function asString(raw, field) {
|
||||
const value = cleanupValue(raw);
|
||||
if (!value) {
|
||||
fail(`Field "${field}" must be a non-empty string.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function asOptionalString(raw) {
|
||||
const value = cleanupValue(raw);
|
||||
return value ? value : undefined;
|
||||
}
|
||||
|
||||
function asDateString(raw, field) {
|
||||
const value = asString(raw, field);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
fail(`Field "${field}" must follow the YYYY-MM-DD format.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseJson(raw, field) {
|
||||
const value = cleanupValue(raw);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
fail(`Field "${field}" must contain valid JSON. ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function asStringArray(raw, field) {
|
||||
const parsed = parseJson(raw, field);
|
||||
if (!Array.isArray(parsed) || parsed.length === 0) {
|
||||
fail(`Field "${field}" must be a non-empty JSON array of strings.`);
|
||||
}
|
||||
parsed.forEach((item, index) => {
|
||||
if (typeof item !== "string" || !item.trim()) {
|
||||
fail(`Field "${field}" must contain only non-empty strings (problem at index ${index}).`);
|
||||
}
|
||||
});
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function asOptionalStringArray(raw, field) {
|
||||
const value = cleanupValue(raw);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseJson(value, field);
|
||||
if (!Array.isArray(parsed)) {
|
||||
fail(`Field "${field}" must be a JSON array of strings.`);
|
||||
}
|
||||
parsed.forEach((item, index) => {
|
||||
if (typeof item !== "string" || !item.trim()) {
|
||||
fail(`Field "${field}" must contain only non-empty strings (problem at index ${index}).`);
|
||||
}
|
||||
});
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function asExamples(raw, field) {
|
||||
const parsed = parseJson(raw, field);
|
||||
if (!Array.isArray(parsed) || parsed.length === 0) {
|
||||
fail(`Field "${field}" must be a non-empty JSON array of objects with "code" and "description" strings.`);
|
||||
}
|
||||
parsed.forEach((item, index) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
fail(`Field "${field}" must contain objects (problem at index ${index}).`);
|
||||
}
|
||||
if (typeof item.code !== "string" || !item.code.trim()) {
|
||||
fail(`Field "${field}" example ${index} is missing a non-empty "code" string.`);
|
||||
}
|
||||
if (typeof item.description !== "string" || !item.description.trim()) {
|
||||
fail(`Field "${field}" example ${index} is missing a non-empty "description" string.`);
|
||||
}
|
||||
});
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function asFlags(raw, field) {
|
||||
const value = cleanupValue(raw);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseJson(value, field);
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
||||
fail(`Field "${field}" must be a JSON object of flag descriptions.`);
|
||||
}
|
||||
Object.entries(parsed).forEach(([flag, description]) => {
|
||||
if (typeof flag !== "string" || !flag.trim()) {
|
||||
fail(`Field "${field}" contains a flag with an invalid name.`);
|
||||
}
|
||||
if (typeof description !== "string" || !description.trim()) {
|
||||
fail(`Field "${field}" flag "${flag}" must have a non-empty description string.`);
|
||||
}
|
||||
});
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function asLinks(raw, field) {
|
||||
const value = cleanupValue(raw);
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = parseJson(value, field);
|
||||
if (!Array.isArray(parsed)) {
|
||||
fail(`Field "${field}" must be a JSON array of link objects.`);
|
||||
}
|
||||
parsed.forEach((item, index) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
fail(`Field "${field}" must contain objects (problem at index ${index}).`);
|
||||
}
|
||||
if (typeof item.label !== "string" || !item.label.trim()) {
|
||||
fail(`Field "${field}" link ${index} is missing a non-empty "label" string.`);
|
||||
}
|
||||
if (typeof item.url !== "string" || !item.url.trim()) {
|
||||
fail(`Field "${field}" link ${index} is missing a non-empty "url" string.`);
|
||||
}
|
||||
});
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function ensureUniqueName(name, toc) {
|
||||
const exists = toc.some((entry) => {
|
||||
if (entry && typeof entry === "object") {
|
||||
if (entry.Name === name) return true;
|
||||
if (Array.isArray(entry.subtoc)) {
|
||||
return entry.subtoc.some((sub) => sub && sub.Name === name);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (exists) {
|
||||
fail(`An entry with the Name "${name}" already exists in toc-source.json.`);
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
fail(`Unable to read or parse JSON file at ${filePath}. ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(filePath, data) {
|
||||
const serialized = JSON.stringify(data, null, 2) + "\n";
|
||||
fs.writeFileSync(filePath, serialized, "utf8");
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user