Skip to content

Add Global Ignore #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 77 additions & 5 deletions src/cmd/lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command } from "@cliffy/command";
import VTConfig from "~/vt/VTConfig.ts";
import VTConfig, { globalConfig } from "~/vt/VTConfig.ts";
import { findVtRoot } from "~/vt/vt/utils.ts";
import { doWithSpinner } from "~/cmd/utils.ts";
import { getNestedProperty, setNestedProperty } from "~/utils.ts";
Expand All @@ -10,6 +10,11 @@ import { printYaml } from "~/cmd/styles.ts";
import { fromError } from "zod-validation-error";
import z from "zod";
import { colors } from "@cliffy/ansi/colors";
import { DEFAULT_WRAP_AMOUNT, GLOBAL_VT_CONFIG_PATH } from "~/consts.ts";
import { join } from "@std/path";
import wrap from "word-wrap";
import { openEditorAt } from "~/cmd/lib/utils/openEditorAt.ts";
import { Select } from "@cliffy/prompt";

function showConfigOptions() {
// deno-lint-ignore no-explicit-any
Expand Down Expand Up @@ -37,6 +42,32 @@ function showConfigOptions() {
printYaml(stringifyYaml(jsonSchema["properties"]));
}

export const configWhereCmd = new Command()
.name("where")
.description("Show the config file locations")
.action(async () => {
// Find project root, if in a Val Town project
let vtRoot: string | undefined = undefined;
try {
vtRoot = await findVtRoot(Deno.cwd());
} catch (_) {
// ignore not found
}

// Local config is always in <root>/.vt/config.yaml
const localConfigPath = vtRoot
? join(vtRoot, ".vt", "config.yaml")
: undefined;

// Just print the resolved paths, always global first, then local if it exists
if (GLOBAL_VT_CONFIG_PATH) {
console.log(GLOBAL_VT_CONFIG_PATH);
}
if (localConfigPath) {
console.log(localConfigPath);
}
});

export const configSetCmd = new Command()
.description("Set a configuration value")
.option("--global", "Set in the global configuration")
Expand All @@ -63,11 +94,11 @@ export const configSetCmd = new Command()

const config = await vtConfig.loadConfig();
const updatedConfig = setNestedProperty(config, key, value);
const oldProperty = getNestedProperty(config, key, value) as
const oldProperty = getNestedProperty(config, key, null) as
| string
| undefined;
| null;

if (oldProperty && oldProperty === value) {
if (oldProperty !== null && oldProperty.toString() === value) {
throw new Error(
`Property ${colors.bold(key)} is already set to ${
colors.bold(oldProperty)
Expand Down Expand Up @@ -101,7 +132,9 @@ export const configSetCmd = new Command()
if (e instanceof z.ZodError) {
throw new Error(
"Invalid input provided! \n" +
colors.red(fromError(e).toString()),
wrap(colors.red(fromError(e).toString()), {
width: DEFAULT_WRAP_AMOUNT,
}),
);
} else throw e;
}
Expand All @@ -113,6 +146,8 @@ export const configGetCmd = new Command()
.description("Get a configuration value")
.arguments("[key]")
.alias("show")
.example("Display current configuration", "vt config get")
.example("Display the API key", "vt config get apiKey")
.action(async (_: unknown, key?: string) => {
await doWithSpinner("Retreiving configuration...", async (spinner) => {
// Check if we're in a Val Town Val directory
Expand Down Expand Up @@ -146,6 +181,41 @@ export const configGetCmd = new Command()
});
});

export const configIgnoreCmd = new Command()
.name("ignore")
.description("Edit or display the global vtignore file")
.option("--no-editor", "Do not open the editor, just display the file path")
.action(async ({ editor }: { editor?: boolean }) => {
const { globalIgnoreFiles } = await globalConfig.loadConfig();

if (!globalIgnoreFiles || globalIgnoreFiles.length === 0) {
console.log("No global ignore files found");
Deno.exit(1);
}

let globalIgnorePath: string;

if (globalIgnoreFiles.length === 1) {
globalIgnorePath = globalIgnoreFiles[0];
} else {
// Use Select prompt if multiple files are available
globalIgnorePath = await Select.prompt({
message: "Select a vtignore file to edit or display",
options: globalIgnoreFiles.map((file) => ({ name: file, value: file })),
});
}

if (!editor) console.log(globalIgnorePath);
else {
const editor = Deno.env.get("EDITOR");
if (editor) {
await openEditorAt(globalIgnorePath);
} else {
console.log(globalIgnorePath);
}
}
});

export const configOptionsCmd = new Command()
.name("options")
.description("List all available configuration options")
Expand All @@ -158,4 +228,6 @@ export const configCmd = new Command()
.description("Manage vt configuration")
.command("set", configSetCmd)
.command("get", configGetCmd)
.command("ignore", configIgnoreCmd)
.command("where", configWhereCmd)
.command("options", configOptionsCmd);
25 changes: 25 additions & 0 deletions src/cmd/lib/utils/openEditorAt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Opens a file at the specified path using the editor defined in the EDITOR environment variable.
* If the EDITOR variable is not set, it simply prints the file path.
*
* @param {string} filePath - The path to the file that should be opened in the editor.
*/
export async function openEditorAt(filePath: string) {
const editor = Deno.env.get("EDITOR");

if (editor) {
const process = new Deno.Command(editor, {
args: [filePath],
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
});

const { status } = process.spawn();
if (!(await status).success) {
console.log(`Failed to open editor ${editor}`);
}
} else {
console.log(filePath);
}
}
2 changes: 2 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export const GLOBAL_VT_CONFIG_PATH = join(xdg.config(), PROGRAM_NAME);
export const DEFAULT_WRAP_WIDTH = 80;
export const MAX_WALK_UP_LEVELS = 100;

export const DEFAULT_WRAP_AMOUNT = 80;

export const FIRST_VERSION_NUMBER = 0;

export const STATUS_STYLES: Record<
Expand Down
1 change: 1 addition & 0 deletions src/vt/lib/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export function pull(params: PullParams): Promise<ItemStatusManager> {
if (!(e instanceof Deno.errors.NotFound)) throw e;
}
}));

return [changes, !dryRun];
},
{ targetDir, prefix: "vt_pull_" },
Expand Down
8 changes: 7 additions & 1 deletion src/vt/lib/utils/ItemStatusManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,13 @@ export class ItemStatusManager {
/**
* JSON object representation of the file state.
*/
public toJSON() {
public toJSON(): {
modified: ModifiedItemStatus[];
not_modified: NotModifiedItemStatus[];
deleted: DeletedItemStatus[];
created: CreatedItemStatus[];
renamed: RenamedItemStatus[];
} {
return {
modified: this.modified,
not_modified: this.not_modified,
Expand Down
17 changes: 12 additions & 5 deletions src/vt/vt/VTMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
META_STATE_FILE_NAME,
} from "~/consts.ts";
import { ALWAYS_IGNORE_PATTERNS } from "~/consts.ts";
import * as path from "@std/path";
import { VTStateSchema } from "~/vt/vt/schemas.ts";
import type { z } from "zod";
import { ensureDir, exists, walk } from "@std/fs";
import { globalConfig } from "~/vt/VTConfig.ts";
import { basename, join } from "@std/path";

/**
* The VTMeta class manages .vt/* configuration files and provides abstractions
Expand All @@ -33,7 +34,7 @@ export default class VTMeta {
* @returns {string} The full file path as a string.
*/
public getVtStateFileName(): string {
return path.join(this.#rootPath, META_FOLDER_NAME, META_STATE_FILE_NAME);
return join(this.#rootPath, META_FOLDER_NAME, META_STATE_FILE_NAME);
}

/**
Expand All @@ -44,15 +45,21 @@ export default class VTMeta {
private async gitignoreFilePaths(): Promise<string[]> {
const ignoreFiles: string[] = [];

// Always add the global .vtignore if it exists
const { globalIgnoreFiles } = await globalConfig.loadConfig();
for (const filePath of globalIgnoreFiles || []) {
if (await exists(filePath)) ignoreFiles.push(filePath);
}

// Walk through all directories recursively starting from root path
for await (const file of walk(this.#rootPath)) {
if (path.basename(file.path) === META_IGNORE_FILE_NAME) {
if (basename(file.path) === META_IGNORE_FILE_NAME) {
if (await exists(file.path)) ignoreFiles.push(file.path);
}
}

// Always include the root meta ignore file if it wasn't found in the walk
const rootMetaIgnore = path.join(this.#rootPath, META_IGNORE_FILE_NAME);
const rootMetaIgnore = join(this.#rootPath, META_IGNORE_FILE_NAME);
if (!ignoreFiles.includes(rootMetaIgnore) && await exists(rootMetaIgnore)) {
ignoreFiles.push(rootMetaIgnore);
}
Expand Down Expand Up @@ -99,7 +106,7 @@ export default class VTMeta {
});

// Ensure the metadata directory exists
await ensureDir(path.join(this.#rootPath, META_FOLDER_NAME));
await ensureDir(join(this.#rootPath, META_FOLDER_NAME));

// Write the meta to file
await Deno.writeTextFile(
Expand Down
17 changes: 16 additions & 1 deletion src/vt/vt/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { join } from "@std/path";
import { z } from "zod";
import { DEFAULT_EDITOR_TEMPLATE } from "~/consts.ts";
import {
DEFAULT_EDITOR_TEMPLATE,
GLOBAL_VT_CONFIG_PATH,
META_IGNORE_FILE_NAME,
} from "~/consts.ts";

/**
* JSON schema for the state.json file for the .vt folder.
Expand Down Expand Up @@ -43,6 +48,15 @@ export const VTConfigSchema = z.object({
message: "API key must be 32-33 characters long when provided",
})
.nullable(),
globalIgnoreFiles: z.preprocess(
(input) => {
if (typeof input === "string") {
return input.split(",").map((s) => s.trim()).filter(Boolean);
}
return input;
},
z.array(z.string()),
).optional(),
dangerousOperations: z.object({
confirmation: z.union([
z.boolean(),
Expand All @@ -54,6 +68,7 @@ export const VTConfigSchema = z.object({

export const DefaultVTConfig: z.infer<typeof VTConfigSchema> = {
apiKey: null,
globalIgnoreFiles: [join(GLOBAL_VT_CONFIG_PATH, META_IGNORE_FILE_NAME)],
dangerousOperations: {
confirmation: true,
},
Expand Down