diff --git a/README.md b/README.md index a06e859..eb11a9d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [Cliffy](https://cliffy.io/) inspired by [GNU Stow](https://www.gnu.org/software/stow/) - ## Table of Contents - [Demo](#demo) @@ -19,6 +18,9 @@ - [Config](#config) - [Add](#add) - [Edit](#edit) + - [Status](#status) + - [Push](#push) + - [Pull](#pull) - [Upgrade](#upgrade) - [Ignore](#ignore) @@ -70,9 +72,8 @@ Examples of structure: └── .zshrc ``` -Dot CLI automatically symlinked all files to your `$HOME` directory -(or any directory you want to target). It follows the structure inside each -package. +Dot CLI automatically symlinked all files to your `$HOME` directory (or any +directory you want to target). It follows the structure inside each package. Example for the **ZSH** package: @@ -126,7 +127,8 @@ Basic setup. Ask you to set locations to your dotfiles and the target location. dot init ``` -**With arguments:** You can clone your dotfiles repository and link all packages. +**With arguments:** You can clone your dotfiles repository and link all +packages. ```bash dot init git@github.com:ArthurMialon/dotfiles.git @@ -251,6 +253,42 @@ dot edit --- +### Status + +Check status of your dotfiles repository. + +**Basic:** + +```bash +dot status +``` + +--- + +### Push + +Push updates to your remote dotfiles repository. + +**Basic:** + +```bash +dot push +``` + +--- + +### Pull + +Pull updates from your remote dotfiles repository and link files. + +**Basic:** + +```bash +dot pull +``` + +--- + ### Upgrade You can upgrade the Dot CLI with the following command. diff --git a/src/commands/init.ts b/src/commands/init.ts index d829618..1392d33 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,7 +1,7 @@ import Dot from "../dot.ts"; import { blue, bold } from "@std/fmt/colors"; import { Command } from "@cliffy/command"; -import { Confirm, Input } from "@cliffy/prompt"; +import { Confirm } from "@cliffy/prompt"; import list from "./list.ts"; import link from "./link.ts"; import configEditPrompt from "../prompt/config-edit.ts"; @@ -10,7 +10,7 @@ import * as log from "../tools/logging.ts"; import * as git from "../tools/git.ts"; export default new Command() - .description("Initialize dot CLI with valid configuration.") + .description(`Initialize ${Dot.title} with valid configuration.`) .arguments("[repository]") .action(async (_flags, remoteRepository) => { log.info(blue(`👋 Welcome to ${Dot.title}.`)); diff --git a/src/commands/pull.ts b/src/commands/pull.ts new file mode 100644 index 0000000..07d205c --- /dev/null +++ b/src/commands/pull.ts @@ -0,0 +1,48 @@ +import * as log from "../tools/logging.ts"; +import * as config from "../tools/config.ts"; +import { Command } from "@cliffy/command"; +import { bold } from "@std/fmt/colors"; +import * as git from "../tools/git.ts"; +import { Confirm } from "@cliffy/prompt"; +import link from "./link.ts"; + +export default new Command() + .description("Pull new version of your dotfiles") + .option( + "-f, --force", + "Force link after pull", + { + default: false, + }, + ) + .action(async ({ force }) => { + const { repo } = await config.get(); + + const branch = await git.getCurrentBranch(repo); + + if (!branch) { + log.error("Cannot read current branch of repository", bold(repo)); + Deno.exit(1); + } + + const success = await git.pull(repo, branch); + + if (!success) { + log.error("Cannot pull your repository in:", bold(repo)); + log.error("Please fix conflicts and/or rebase."); + Deno.exit(1); + } + + log.info("\n"); + + const confirm = force ? true : (await Confirm.prompt({ + message: `Do you want to link new changes?`, + })); + + if (!confirm) { + log.info("Apply changes with `dot link` whenever you want."); + Deno.exit(0); + } + + await link.parse(force ? ["-f"] : []); + }); diff --git a/src/commands/push.ts b/src/commands/push.ts new file mode 100644 index 0000000..f84efe6 --- /dev/null +++ b/src/commands/push.ts @@ -0,0 +1,52 @@ +import * as log from "../tools/logging.ts"; +import * as config from "../tools/config.ts"; +import { Command } from "@cliffy/command"; +import status from "./status.ts"; +import { bold } from "@std/fmt/colors"; +import * as git from "../tools/git.ts"; +import { Confirm } from "@cliffy/prompt"; + +function toISODate(date: Date): string { + return date.toISOString().split("T")[0]; +} + +export default new Command() + .description("Publish new version of your dotfiles") + .action(async () => { + const { repo } = await config.get(); + + await status.parse([]); + + const changes = await git.hasChange(repo); + + if (!changes) { + log.info("No changes to push"); + Deno.exit(0); + } + + const branch = await git.getCurrentBranch(repo); + + if (!branch) { + log.error("Cannot read current branch of repository", bold(repo)); + Deno.exit(1); + } + + const date = toISODate(new Date()); + const commit = `chore: updated on ${date} from Dot CLI`; + + log.info("\nCommit message", bold(commit)); + + const confirm = await Confirm.prompt({ + message: `Do you want to commit and push the changes?`, + }); + + if (!confirm) { + log.info("Commit aborted"); + Deno.exit(0); + } + + await git.commit(repo, commit); + await git.push(repo, branch); + + log.success("Dotfiles pushed to remote repository."); + }); diff --git a/src/commands/status.ts b/src/commands/status.ts new file mode 100644 index 0000000..ae8b4cd --- /dev/null +++ b/src/commands/status.ts @@ -0,0 +1,16 @@ +import * as log from "../tools/logging.ts"; +import * as config from "../tools/config.ts"; +import { Command } from "@cliffy/command"; +import { bold } from "@std/fmt/colors"; +import * as git from "../tools/git.ts"; + +export default new Command() + .description("Check status of your dotfiles repository") + .action(async () => { + const configuration = await config.get(); + + log.info("Status of your dotfiles in:"); + log.info(bold(configuration.repo), "\n"); + + await git.status(configuration.repo); + }); diff --git a/src/commands/unlink.ts b/src/commands/unlink.ts index 6b7b5b9..4bd99a1 100644 --- a/src/commands/unlink.ts +++ b/src/commands/unlink.ts @@ -21,7 +21,7 @@ export default new Command() const configuration = await config.get(); const pkgs = await packages.list(configuration.repo) - .catch(() => []) + .catch(() => []); const filteredPkgs = pkgs .filter((pkg) => { diff --git a/src/main.ts b/src/main.ts index 822fbf9..ceb85d7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ -import Dot from "./dot.ts"; import { Command } from "@cliffy/command"; +import Dot from "./dot.ts"; import config from "./commands/config/index.ts"; import init from "./commands/init.ts"; import list from "./commands/list.ts"; @@ -9,7 +9,9 @@ import link from "./commands/link.ts"; import unlink from "./commands/unlink.ts"; import add from "./commands/add.ts"; import upgrade from "./commands/upgrade.ts"; - +import status from "./commands/status.ts"; +import push from "./commands/push.ts"; +import pull from "./commands/pull.ts"; import * as log from "./tools/logging.ts"; const command = new Command() @@ -24,6 +26,9 @@ const command = new Command() .command("link", link) .command("unlink", unlink) .command("upgrade", upgrade) - .command("add", add); + .command("add", add) + .command("status", status) + .command("pull", pull) + .command("push", push); await command.parse(Deno.args); diff --git a/src/tools/git.ts b/src/tools/git.ts index a515588..720ba23 100644 --- a/src/tools/git.ts +++ b/src/tools/git.ts @@ -1,19 +1,121 @@ import { copy, readerFromStreamReader } from "@std/io"; +import * as log from "../tools/logging.ts"; export const clone = async ( repository: string, targetFolder: string, ): Promise => { - const clone = new Deno.Command("git", { - args: ["clone", repository, targetFolder], + const command = new Deno.Command("git", { + args: ["command", repository, targetFolder], stdout: "piped", stderr: "piped", }).spawn(); - copy(readerFromStreamReader(clone.stdout.getReader()), Deno.stdout); - copy(readerFromStreamReader(clone.stderr.getReader()), Deno.stderr); + copy(readerFromStreamReader(command.stdout.getReader()), Deno.stdout); + copy(readerFromStreamReader(command.stderr.getReader()), Deno.stderr); - const { success } = await clone.status; + const { success } = await command.status; + + return !!success; +}; + +const statusCommand = (repository: string) => { + return new Deno.Command("git", { + args: ["-C", repository, "status", "--short"], + stdout: "piped", + stderr: "piped", + }).spawn(); +}; + +export const status = async ( + repository: string, +): Promise => { + const command = statusCommand(repository); + + copy(readerFromStreamReader(command.stdout.getReader()), Deno.stdout); + copy(readerFromStreamReader(command.stderr.getReader()), Deno.stderr); + + const { success } = await command.status; + + return !!success; +}; + +export const hasChange = async ( + repository: string, +): Promise => { + const command = statusCommand(repository); + + const output = await command.output(); + + const status = new TextDecoder().decode(output.stdout); + + return !!Number(status.trim().length); +}; + +export const commit = async ( + repository: string, + message: string, +): Promise => { + const command = new Deno.Command("git", { + args: ["-C", repository, "commit", "-am", message], + stdout: "piped", + stderr: "piped", + }).spawn(); + + copy(readerFromStreamReader(command.stdout.getReader()), Deno.stdout); + copy(readerFromStreamReader(command.stderr.getReader()), Deno.stderr); + + const { success } = await command.status; + + return !!success; +}; + +export const getCurrentBranch = async (repository: string) => { + const command = new Deno.Command("git", { + args: ["-C", repository, "rev-parse", "--abbrev-ref", "HEAD"], + stdout: "piped", + stderr: "piped", + }).spawn(); + + const output = await command.output(); + + const branch = new TextDecoder().decode(output.stdout); + + return branch.trim(); +}; + +export const push = async ( + repository: string, + branch: string, +): Promise => { + const command = new Deno.Command("git", { + args: ["-C", repository, "push", "origin", branch], + stdout: "piped", + stderr: "piped", + }).spawn(); + + copy(readerFromStreamReader(command.stdout.getReader()), Deno.stdout); + copy(readerFromStreamReader(command.stderr.getReader()), Deno.stderr); + + const { success } = await command.status; + + return !!success; +}; + +export const pull = async ( + repository: string, + branch: string, +): Promise => { + const command = new Deno.Command("git", { + args: ["-C", repository, "pull", "origin", branch], + stdout: "piped", + stderr: "piped", + }).spawn(); + + copy(readerFromStreamReader(command.stdout.getReader()), Deno.stdout); + copy(readerFromStreamReader(command.stderr.getReader()), Deno.stderr); + + const { success } = await command.status; return !!success; }; diff --git a/src/tools/packages.ts b/src/tools/packages.ts index e4f2848..23ba5b8 100644 --- a/src/tools/packages.ts +++ b/src/tools/packages.ts @@ -1,6 +1,5 @@ import type { DotConfig } from "./config.ts"; import { dirname, join } from "@std/path"; -import { exists } from "@std/fs"; import Dot from "../dot.ts"; import { IgnoreFile } from "../tools/ignore.ts"; @@ -53,14 +52,6 @@ const listSourceFiles = async ( return result; }; -const getTargetLink = async ( - path: string, -): Promise => { - if (await exists(path)) return Deno.realPath(path); - - return null; -}; - const getPackage = async ( path: string, name: string, @@ -100,7 +91,7 @@ export const linkPackage = async ( ); // Avoid throw if it's a symlink that doesn't point to valid file - await Deno.remove(targetLinkPath).catch(() => {}) + await Deno.remove(targetLinkPath).catch(() => {}); await Deno.mkdir(dirname(targetLinkPath), { recursive: true }); await Deno.symlink(file.fullPath, targetLinkPath); } @@ -119,6 +110,6 @@ export const unlinkPackage = async ( file.name, ); - await Deno.remove(targetLinkPath).catch(() => {}) + await Deno.remove(targetLinkPath).catch(() => {}); } };