From 35f2fe646f220c469e41c4ebae0c83504484a0f5 Mon Sep 17 00:00:00 2001 From: "a.stevan" Date: Mon, 9 Oct 2023 11:18:58 +0200 Subject: [PATCH 01/18] remove everything --- docs/README.md | 3 - docs/installation/README.md | 7 - docs/installation/manual.md | 22 - docs/installation/nupm.md | 16 - nu-git-manager/mod.nu | 2 - nu-git-manager/src/gm/mod.nu | 169 -------- nu-git-manager/src/gm/utils/utils.nu | 160 ------- nu-git-manager/src/sugar/completions/git.nu | 457 -------------------- nu-git-manager/src/sugar/completions/mod.nu | 0 nu-git-manager/src/sugar/dotfiles.nu | 28 -- nu-git-manager/src/sugar/gh.nu | 227 ---------- nu-git-manager/src/sugar/gist.nu | 137 ------ nu-git-manager/src/sugar/git.nu | 202 --------- nu-git-manager/src/sugar/mod.nu | 1 - toolkit/mod.nu | 43 -- 15 files changed, 1474 deletions(-) delete mode 100644 docs/README.md delete mode 100644 docs/installation/README.md delete mode 100644 docs/installation/manual.md delete mode 100644 docs/installation/nupm.md delete mode 100644 nu-git-manager/mod.nu delete mode 100644 nu-git-manager/src/gm/mod.nu delete mode 100644 nu-git-manager/src/gm/utils/utils.nu delete mode 100644 nu-git-manager/src/sugar/completions/git.nu delete mode 100644 nu-git-manager/src/sugar/completions/mod.nu delete mode 100644 nu-git-manager/src/sugar/dotfiles.nu delete mode 100644 nu-git-manager/src/sugar/gh.nu delete mode 100644 nu-git-manager/src/sugar/gist.nu delete mode 100644 nu-git-manager/src/sugar/git.nu delete mode 100644 nu-git-manager/src/sugar/mod.nu delete mode 100644 toolkit/mod.nu diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 1ee395d1..00000000 --- a/docs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## official documentation of `nu-git-manager` - -- :recycle: install `nu-git-manager` [here](installation/) diff --git a/docs/installation/README.md b/docs/installation/README.md deleted file mode 100644 index 0ef9248f..00000000 --- a/docs/installation/README.md +++ /dev/null @@ -1,7 +0,0 @@ -> **Warning** -> make sure you have the dependencies installed as specified in -> [`packages.nuon`](../../package.nuon) - -currently, there are two ways to install `nu-git-manager` -- the [manual process](manual.md) -- with the [`nupm` package manager](nupm.md) diff --git a/docs/installation/manual.md b/docs/installation/manual.md deleted file mode 100644 index 3be56577..00000000 --- a/docs/installation/manual.md +++ /dev/null @@ -1,22 +0,0 @@ -one way to install `nu-git-manager` right now is the following manual process - -> **Note** -> let's say you have defined the following environment variable -> ```nu -> # in `$nu.env-path` -> $env.NU_LIB_PATH = "/path/to/libs" -> ``` - -- clone the repo to a location you want it to be -```nu -git clone https://github.com/amtoine/nu-git-manager.git ($env.NU_LIB_PATH | path join "nu-git-manager") -``` -- make it loadable in your `NU_LIB_DIRS` -```nu -# in `$nu.env-path` -$env.NU_LIB_DIRS = ($env.NU_LIB_DIRS | append $env.NU_LIB_PATH) -``` -- update it regularly to have the latest version -```nu -git -C ($env.NU_LIB_PATH | path join "nu-git-manager") pull -``` diff --git a/docs/installation/nupm.md b/docs/installation/nupm.md deleted file mode 100644 index 911607a1..00000000 --- a/docs/installation/nupm.md +++ /dev/null @@ -1,16 +0,0 @@ -i like to use the `nupm` package manager for Nushell :yum: - -- first install the package manager [here](https://github.com/amtoine/nupm/tree/main#recycle-installation) -- then install `nu-git-manager` with a simple -```nu -nupm install https://github.com/amtoine/nu-git-manager.git -``` -- finally you can activate commands and modules with something like -```nu -nupm activate nu-git-manager gm -nupm activate nu-git-manager sugar git -nupm activate nu-git-manager sugar gh -nupm activate nu-git-manager sugar gist -nupm activate nu-git-manager sugar completions git * -nupm activate nu-git-manager sugar dotfiles -``` diff --git a/nu-git-manager/mod.nu b/nu-git-manager/mod.nu deleted file mode 100644 index ac0b1bc6..00000000 --- a/nu-git-manager/mod.nu +++ /dev/null @@ -1,2 +0,0 @@ -export module src/gm/ -export module src/sugar/ diff --git a/nu-git-manager/src/gm/mod.nu b/nu-git-manager/src/gm/mod.nu deleted file mode 100644 index d744cb35..00000000 --- a/nu-git-manager/src/gm/mod.nu +++ /dev/null @@ -1,169 +0,0 @@ -use std log -use utils/utils.nu [ - "get root dir" - "parse project" - "default project" - "pick repo" - "list repos" -] - -# fuzzy-jump to any repository managed by `gm` -export def-env goto [ - query?: string # a search query to narrow down the list of choices -] { - let choice = (pick repo - $"Please (ansi yellow_italic)choose a repo(ansi reset) to (ansi green_underline)jump to:(ansi reset)" - $query - ) - if ($choice | is-empty) { - return - } - - cd (get root dir | path join $choice) -} - -# fuzzy-delete any repository managed by `gm` -export def remove [ - query?: string # a search query to narrow down the list of choices - --force (-f) # do not ask for comfirmation when deleting a repository -] { - let choice = (pick repo - $"Please (ansi yellow_italic)choose a repo(ansi reset) to (ansi red_underline)completely remove:(ansi reset)" - $query - ) - if ($choice | is-empty) { - return - } - - let repo = (get root dir | path join $choice) - if $force { - rm --trash --verbose --recursive $repo - } else { - rm --trash --verbose --recursive $repo --interactive - } -} - -# TODO: add support for other hosts than github -# TODO: better worktree support - -# Clone a repository into a standard location -# -# This place is organised by domain and path. -export def grab [ - project: string # |//|/| - --ssh (-p) # use ssh instead of https. - --bare (-b) # clone as *bare* repo (specific to worktrees). - --update (-u) # not supported - --shallow (-s) # not supported - --branch # not supported - --no-recursive # not supported - --look # not supported - --silent # not supported - --vcs (-v): string # not supported -] { - # TODO: implement `--update` option - if $update { - log warning "`--update` option for `gm grab` COMING SOON" - } - # TODO: implement `--shallow` option - if $shallow { - log warning "`--shallow` option for `gm grab` COMING SOON" - } - # TODO: implement `--branch` option - if $branch { - log warning "`--branch` option for `gm grab` COMING SOON" - } - # TODO: implement `--look` option - if $look { - log warning "`--look` option for `gm grab` COMING SOON" - } - # TODO: implement `--silent` option - if $silent { - log warning "`--silent` option for `gm grab` COMING SOON" - } - # TODO: implement `--no-recursive` option - if $no_recursive { - log warning "`--no-recursive` option for `gm grab` COMING SOON" - } - if ($vcs | is-empty) { - log debug "`--vcs` option is NOT SUPPORTED in `gm grab`" - } - - let span = metadata $project | get span - - let project = ( - parse project $project - | default project - | update project { str replace --regex --all '\/' '-'} - ) - - let url = if $ssh { - $"git@($project.host):($project.user)/($project.project).git" - } else { - $"https://($project.host)/($project.user)/($project.project).git" - } - - let local = (get root dir | path join $project.host $project.user $project.project) - - if ($local | path exists) { - error make { - msg: $"(ansi red)repo_already_grabbed(ansi reset)" - label: { - text: "this repo has already been grabbed" - start: $span.start - end: $span.end - } - } - } - - if $bare { - git clone --bare --recurse-submodules $url $local - } else { - git clone --recurse-submodules $url $local - } -} - -# list locally-cloned repositories -# -# by default `gm list` only searches the three first depth levels: -# - host -# - user -# - project -export def list [ - query?: string # return only repositories matching the query - --exact (-e) # force the match to be exact, i.e. the query equals to project, user/project or host/user/project - --full-path (-p) # return the full paths instead of path relative to the `gm` root - --recursive # perform a recursive search of all `.git/` directories -] {( - list repos $query - --exact $exact - --full-path $full_path - --recursive $recursive -)} - -# print the root of the repositories -export def root [ - --all (-a) # not supported -] { - if $all { - log debug "`--all` option is NOT SUPPORTED in `gm root`" - } - - get root dir -} - -# create a new repository -export def create [ - repository: string # |//|/| - --vcs (-v): string # not supported -] { - if ($vcs | is-empty) { - log debug "`--vcs` option is NOT SUPPORTED in `gm create`" - } - - # TODO: implement `gm create` - log warning "COMING SOON" -} - -# the `nu-[g]it-[m]anager`, a WIP to manage any `git` repo in a centralized store, with sugar on top -export def main [] { help gm } diff --git a/nu-git-manager/src/gm/utils/utils.nu b/nu-git-manager/src/gm/utils/utils.nu deleted file mode 100644 index bc7e7b2d..00000000 --- a/nu-git-manager/src/gm/utils/utils.nu +++ /dev/null @@ -1,160 +0,0 @@ -export def "get root dir" [] { - $env.GIT_REPOS_HOME? | default ( - $env.XDG_DATA_HOME? - | default ($env.HOME | path join ".local" "share") - | path join "nu-git-manager" - ) -} - -# Replace all backslashes with forward slashes. -export def "replace slashes" [] { - str replace --all '\' '/' -} - -# parse-project -> record -# parse-project // -> record -# parse-project / -> record -# parse-project -> record -export def "parse project" [ - project: string # |//|/| -] { - let project = ( - $project - | str replace --regex '.git$' '' - | str replace --regex '^http://' '' - | str replace --regex '^https://' '' - | str replace --regex '^ssh://' '' - | str replace --regex '^git@' '' - | str replace --regex --all ':' '/' - | str replace --regex --all '\/+' '/' - | str trim -c '/' - ) - - let hup = ($project | parse "{host}/{user}/{project}") - if not ($hup | is-empty) { - return ($hup | into record) - } - - let up = ($project | parse "{user}/{project}") - if not ($up | is-empty) { - return ($up | into record) - } - - {project: $project} -} - -export def "default project" [] { - default (git config --global user.name) user - | default "github.com" host -} - -export def "list repos" [ - query?: string - --exact: bool = false - --full-path: bool = false - --recursive: bool = false -] { - let root = (get root dir) - let repos = ( - ls ($root | if $recursive { path join "**" "*" ".git" } else { path join "*" "*" "*"}) - | get name - | replace slashes - | str replace --regex $"^($root | replace slashes)" "" - | str replace --regex $".git$" "" - | str trim -l -c '/' - | parse "{host}/{user}/{project}" - | insert user-project {|it| [$it.user $it.project] | path join} - | insert host-user-project {|it| [$it.host $it.user $it.project] | path join} - ) - - let repos = ($repos | if $query != null { - if $exact { - where {|it| ( - ($it.project == $query) or - ($it.user-project == $query) or - ($it.host-user-project == $query) - )} - } else { - find $query - } - } else {}) - - $repos | get host-user-project | if $full_path { - each {|repo| $root | path join $repo} - } else {} -} - -export def "pick repo" [ - prompt: string - query: string -] { - list repos --exact false --full-path false --recursive false - | if $query == null {} else { find $query } - | input list --fuzzy $prompt -} - -#[cfg(test)] -export module tests { - use std assert - use std log - - #[test] - export def parse-project-test [] { - log debug "testing empty input" - assert equal (parse project "") {project: ""} - - # normal parsing - log debug "testing some normal parsing" - let expected = {host: "host", user: "user", project: "project"} - assert equal (parse project "/host/user/project") $expected - assert equal (parse project "host/user/project/") $expected - assert equal (parse project "host//user/project") $expected - assert equal (parse project "host/user/project") $expected - assert equal (parse project "host/user/project.git") $expected - assert equal (parse project "http://host/user/project") $expected - assert equal (parse project "https://host/user/project") $expected - assert equal (parse project "ssh://host/user/project") $expected - assert equal (parse project "git@host:user/project.git") $expected - - # subgroups? - log debug "testing parsing of subgroups" - assert equal (parse project "host/user/group/subgroup/subsubgroup/project") { - host: "host", user: "user", project: "group/subgroup/subsubgroup/project" - } - - # default values... - log debug "testing missing fields" - assert equal (parse project "user/project") {user: "user", project: "project"} - assert equal (parse project "project") {project: "project"} - - # ... with subgroups? - # we cannot parse these properly, that will throw a runtime HTTP error - log debug "testing imperfect subgroups" - assert equal (parse project "user/group/subgroup/subsubgroup/project") { - host: "user", user: "group", project: "subgroup/subsubgroup/project" - } - assert equal (parse project "group/subgroup/subsubgroup/project") { - host: "group", user: "subgroup", project: "subsubgroup/project" - } - - log debug "testing invalid project name" - assert equal (parse project "git#host:user/project.git") { - host: "git#host", user: "user", project: "project" - } - } - - def default-project-test-template [] { - assert equal ($in | default project | columns | sort) ["host" "project" "user"] - } - - #[test] - export def default-project-test [] { - for project in [ - {project: "foo"} - {project: "foo", user: "bar"} - {project: "foo", user: "bar", host: "baz"} - ] { - $project | default-project-test-template - } - } -} diff --git a/nu-git-manager/src/sugar/completions/git.nu b/nu-git-manager/src/sugar/completions/git.nu deleted file mode 100644 index 6865fe07..00000000 --- a/nu-git-manager/src/sugar/completions/git.nu +++ /dev/null @@ -1,457 +0,0 @@ -def is-git-repo [] { - not (do -i { - git rev-parse --is-inside-work-tree - } | is-empty) -} - -def "nu-complete git available upstream" [] { - if not (is-git-repo) { return } - - ^git branch -a | lines | each { |line| $line | str replace --regex '\* ' "" | str trim } -} - -def "nu-complete git remotes" [] { - if not (is-git-repo) { return } - - ^git remote | lines | each { |line| $line | str trim } -} - -def "nu-complete git log" [] { - if not (is-git-repo) { return } - - ^git log --pretty=%h | lines | each { |line| $line | str trim } -} - -# Yield all existing commits in descending chronological order. -def "nu-complete git commits all" [] { - if not (is-git-repo) { return } - - ^git rev-list --all --remotes --pretty=oneline | lines | parse "{value} {description}" -} - -# Yield commits of current branch only. This is useful for e.g. cut points in -# `git rebase`. -def "nu-complete git commits current branch" [] { - if not (is-git-repo) { return } - - ^git log --pretty="%h %s" | lines | parse "{value} {description}" -} - -# Yield local branches like `main`, `feature/typo_fix` -def "nu-complete git local branches" [] { - if not (is-git-repo) { return } - - ^git branch | lines | each { |line| $line | str replace --regex '\* ' "" | str trim } -} - -# Yield remote branches like `origin/main`, `upstream/feature-a` -def "nu-complete git remote branches with prefix" [] { - if not (is-git-repo) { return } - - ^git branch -r | lines | parse -r '^\*?(\s*|\s*\S* -> )(?P\S*$)' | get branch | uniq -} - -# Yield remote branches *without* prefix which do not have a local counterpart. -# E.g. `upstream/feature-a` as `feature-a` to checkout and track in one command -# with `git checkout` or `git switch`. -def "nu-complete git remote branches nonlocal without prefix" [] { - if not (is-git-repo) { return } - - # Get regex to strip remotes prefixes. It will look like `(origin|upstream)` - # for the two remotes `origin` and `upstream`. - let remotes_regex = (["(", ((nu-complete git remotes | each {|r| [$r, '/'] | str join}) | str join "|"), ")"] | str join) - let local_branches = (nu-complete git local branches) - ^git branch -r | lines | parse -r (['^[\* ]+', $remotes_regex, '?(?P\S+)'] | flatten | str join) | get branch | uniq | where {|branch| $branch != "HEAD"} | where {|branch| $branch not-in $local_branches } -} - -def "nu-complete git switch" [] { - if not (is-git-repo) { return } - - (nu-complete git local branches) - | parse "{value}" - | insert description "local branch" - | append (nu-complete git remote branches nonlocal without prefix - | parse "{value}" - | insert description "remote branch") -} - -def "nu-complete git checkout" [] { - if not (is-git-repo) { return } - - (nu-complete git local branches) - | parse "{value}" - | insert description "local branch" - | append (nu-complete git remote branches nonlocal without prefix - | parse "{value}" - | insert description "remote branch") - | append (nu-complete git remote branches with prefix - | parse "{value}" - | insert description "remote branch") - | append (nu-complete git commits all) -} - -# Arguments to `git rebase --onto ` -def "nu-complete git rebase" [] { - if not (is-git-repo) { return } - - (nu-complete git local branches) - | parse "{value}" - | insert description "local branch" - | append (nu-complete git remote branches with prefix - | parse "{value}" - | insert description "remote branch") - | append (nu-complete git commits all) -} - -def "nu-complete git stash-list" [] { - if not (is-git-repo) { return } - - git stash list | lines | parse "{value}: {description}" -} - -def "nu-complete git tags" [] { - if not (is-git-repo) { return } - - ^git tag | lines -} - -def "nu-complete git built-in-refs" [] { - if not (is-git-repo) { return } - - [HEAD FETCH_HEAD ORIG_HEAD] -} - -def "nu-complete git refs" [] { - if not (is-git-repo) { return } - - nu-complete git switchable branches - | parse "{value}" - | insert description Branch - | append (nu-complete git tags | parse "{value}" | insert description Tag) - | append (nu-complete git built-in-refs) -} - -def "nu-complete git subcommands" [] { - if not (is-git-repo) { return } - - ^git help -a | lines | where $it starts-with " " | parse -r '\s*(?P[^ ]+) \s*(?P\w.*)' -} - -# Check out git branches and files -export extern "git checkout" [ - ...targets: string@"nu-complete git checkout" # name of the branch or files to checkout - --conflict: string # conflict style (merge or diff3) - --detach(-d) # detach HEAD at named commit - --force(-f) # force checkout (throw away local modifications) - --guess # second guess 'git checkout ' (default) - --ignore-other-worktrees # do not check if another worktree is holding the given ref - --ignore-skip-worktree-bits # do not limit pathspecs to sparse entries only - --merge(-m) # perform a 3-way merge with the new branch - --orphan: string # new unparented branch - --ours(-2) # checkout our version for unmerged files - --overlay # use overlay mode (default) - --overwrite-ignore # update ignored files (default) - --patch(-p) # select hunks interactively - --pathspec-from-file: string # read pathspec from file - --progress # force progress reporting - --quiet(-q) # suppress progress reporting - --recurse-submodules: string # control recursive updating of submodules - --theirs(-3) # checkout their version for unmerged files - --track(-t) # set upstream info for new branch - -b: string # create and checkout a new branch - -B: string # create/reset and checkout a branch - -l # create reflog for new branch -] - -# Download objects and refs from another repository -export extern "git fetch" [ - repository?: string@"nu-complete git remotes" # name of the branch to fetch - --all # Fetch all remotes - --append(-a) # Append ref names and object names to .git/FETCH_HEAD - --atomic # Use an atomic transaction to update local refs. - --depth: int # Limit fetching to n commits from the tip - --deepen: int # Limit fetching to n commits from the current shallow boundary - --shallow-since: string # Deepen or shorten the history by date - --shallow-exclude: string # Deepen or shorten the history by branch/tag - --unshallow # Fetch all available history - --update-shallow # Update .git/shallow to accept new refs - --negotiation-tip: string # Specify which commit/glob to report while fetching - --negotiate-only # Do not fetch, only print common ancestors - --dry-run # Show what would be done - --write-fetch-head # Write fetched refs in FETCH_HEAD (default) - --no-write-fetch-head # Do not write FETCH_HEAD - --force(-f) # Always update the local branch - --keep(-k) # Keep dowloaded pack - --multiple # Allow several arguments to be specified - --auto-maintenance # Run 'git maintenance run --auto' at the end (default) - --no-auto-maintenance # Don't run 'git maintenance' at the end - --auto-gc # Run 'git maintenance run --auto' at the end (default) - --no-auto-gc # Don't run 'git maintenance' at the end - --write-commit-graph # Write a commit-graph after fetching - --no-write-commit-graph # Don't write a commit-graph after fetching - --prefetch # Place all refs into the refs/prefetch/ namespace - --prune(-p) # Remove obsolete remote-tracking references - --prune-tags(-P) # Remove any local tags that do not exist on the remote - --no-tags(-n) # Disable automatic tag following - --refmap: string # Use this refspec to map the refs to remote-tracking branches - --tags(-t) # Fetch all tags - --recurse-submodules: string # Fetch new commits of populated submodules (yes/on-demand/no) - --jobs(-j): int # Number of parallel children - --no-recurse-submodules # Disable recursive fetching of submodules - --set-upstream # Add upstream (tracking) reference - --submodule-prefix: string # Prepend to paths printed in informative messages - --upload-pack: string # Non-default path for remote command - --quiet(-q) # Silence internally used git commands - --verbose(-v) # Be verbose - --progress # Report progress on stderr - --server-option(-o): string # Pass options for the server to handle - --show-forced-updates # Check if a branch is force-updated - --no-show-forced-updates # Don't check if a branch is force-updated - -4 # Use IPv4 addresses, ignore IPv6 addresses - -6 # Use IPv6 addresses, ignore IPv4 addresses -] - -# Push changes -export extern "git push" [ - remote?: string@"nu-complete git remotes", # the name of the remote - ...refs: string@"nu-complete git local branches" # the branch / refspec - --all # push all refs - --atomic # request atomic transaction on remote side - --delete(-d) # delete refs - --dry-run(-n) # dry run - --exec: string # receive pack program - --follow-tags # push missing but relevant tags - --force-with-lease # require old value of ref to be at this value - --force(-f) # force updates - --ipv4(-4) # use IPv4 addresses only - --ipv6(-6) # use IPv6 addresses only - --mirror # mirror all refs - --no-verify # bypass pre-push hook - --porcelain # machine-readable output - --progress # force progress reporting - --prune # prune locally removed refs - --push-option(-o): string # option to transmit - --quiet(-q) # be more quiet - --receive-pack: string # receive pack program - --recurse-submodules: string # control recursive pushing of submodules - --repo: string # repository - --set-upstream(-u) # set upstream for git pull/status - --signed: string # GPG sign the push - --tags # push tags (can't be used with --all or --mirror) - --thin # use thin pack - --verbose(-v) # be more verbose -] - -# Pull changes -export extern "git pull" [ - remote?: string@"nu-complete git remotes", # the name of the remote - ...refs: string@"nu-complete git local branches" # the branch / refspec - --rebase # rebase current branch on top of upstream after fetching -] - -# Switch between branches and commits -export extern "git switch" [ - switch?: string@"nu-complete git switch" # name of branch to switch to - --create(-c): string # create a new branch - --detach(-d): string@"nu-complete git log" # switch to a commit in a detatched state - --force-create(-C): string # forces creation of new branch, if it exists then the existing branch will be reset to starting point - --force(-f) # alias for --discard-changes - --guess # if there is no local branch which matches then name but there is a remote one then this is checked out - --ignore-other-worktrees # switch even if the ref is held by another worktree - --merge(-m) # attempts to merge changes when switching branches if there are local changes - --no-guess # do not attempt to match remote branch names - --no-progress # do not report progress - --no-recurse-submodules # do not update the contents of sub-modules - --no-track # do not set "upstream" configuration - --orphan: string # create a new orphaned branch - --progress # report progress status - --quiet(-q) # suppress feedback messages - --recurse-submodules # update the contents of sub-modules - --track(-t) # set "upstream" configuration -] - -# Apply the change introduced by an existing commit -export extern "git cherry-pick" [ - ...commit: string@"nu-complete git commits all" # The commit ID to be cherry-picked - --edit(-e) # Edit the commit message prior to committing - --no-commit(-n) # Apply changes without making any commit - --signoff(-s) # Add Signed-off-by line to the commit message - --ff # Fast-forward if possible - --continue # Continue the operation in progress - --abort # Cancel the operation - --skip # Skip the current commit and continue with the rest of the sequence -] - -# Rebase the current branch -export extern "git rebase" [ - branch?: string@"nu-complete git rebase" # name of the branch to rebase onto - upstream?: string@"nu-complete git rebase" # upstream branch to compare against - --continue # restart rebasing process after editing/resolving a conflict - --abort # abort rebase and reset HEAD to original branch - --quit # abort rebase but do not reset HEAD - --interactive(-i) # rebase interactively with list of commits in editor - --onto?: string@"nu-complete git rebase" # starting point at which to create the new commits - --root # start rebase from root commit -] - -# List or change branches -export extern "git branch" [ - branch?: string@"nu-complete git local branches" # name of branch to operate on - --abbrev # use short commit hash prefixes - --edit-description # open editor to edit branch description - --merged # list reachable branches - --no-merged # list unreachable branches - --set-upstream-to: string@"nu-complete git available upstream" # set upstream for branch - --unset-upstream # remote upstream for branch - --all # list both remote and local branches - --copy # copy branch together with config and reflog - --format # specify format for listing branches - --move # rename branch - --points-at # list branches that point at an object - --show-current # print the name of the current branch - --verbose # show commit and upstream for each branch - --color # use color in output - --quiet # suppress messages except errors - --delete(-d) # delete branch - --list # list branches - --contains: string@"nu-complete git commits all" # show only branches that contain the specified commit - --no-contains # show only branches that don't contain specified commit - --track(-t) # when creating a branch, set upstream -] - -# List or change tracked repositories -export extern "git remote" [ - --verbose(-v) # Show URL for remotes -] - -# Add a new tracked repository -export extern "git remote add" [ -] - -# Rename a tracked repository -export extern "git remote rename" [ - remote: string@"nu-complete git remotes" # remote to rename - new_name: string # new name for remote -] - -# Remove a tracked repository -export extern "git remote remove" [ - remote: string@"nu-complete git remotes" # remote to remove -] - -# Get the URL for a tracked repository -export extern "git remote get-url" [ - remote: string@"nu-complete git remotes" # remote to get URL for -] - -# Set the URL for a tracked repository -export extern "git remote set-url" [ - remote: string@"nu-complete git remotes" # remote to set URL for - url: string # new URL for remote -] - -# Show changes between commits, working tree etc -export extern "git diff" [ - rev1?: string@"nu-complete git refs" - rev2?: string@"nu-complete git refs" - --cached # show staged changes - --name-only # only show names of changed files - --name-status # show changed files and kind of change - --no-color # disable color output -] - -# Commit changes -export extern "git commit" [ - --all(-a) # automatically stage all modified and deleted files - --amend # amend the previous commit rather than adding a new one - --message(-m): string # specify the commit message rather than opening an editor - --no-edit # don't edit the commit message (useful with --amend) -] - -# List commits -export extern "git log" [ - # Ideally we'd allow completion of revisions here, but that would make completion of filenames not work. - -U # show diffs - --follow # show history beyond renames (single file only) - --grep: string # show log entries matching supplied regular expression -] - -# Show or change the reflog -export extern "git reflog" [ -] - -# Stage files -export extern "git add" [ - --patch(-p) # interactively choose hunks to stage -] - -# Delete file from the working tree and the index -export extern "git rm" [ - -r # recursive -] - -# Show the working tree status -export extern "git status" [ - --verbose(-v) # verbose -] - -# Stash changes for later -export extern "git stash push" [ - --patch(-p) # interactively choose hunks to stash -] - -# Unstash previously stashed changes -export extern "git stash pop" [ -] - -# List stashed changes -export extern "git stash list" [ -] - -# Show a stashed change -export extern "git stash show" [ - stash: string@"nu-complete git stash-list" - -U # show diff -] - -# Drop a stashed change -export extern "git stash drop" [ - stash: string@"nu-complete git stash-list" -] - -# Create a new git repository -export extern "git init" [ - --initial-branch(-b) # initial branch name -] - -# List or manipulate tags -export extern "git tag" [ - --delete(-d): string@"nu-complete git tags" # delete a tag -] - -# Start a binary search to find the commit that introduced a bug -export extern "git bisect start" [ - bad?: string # a commit that has the bug - good?: string # a commit that doesn't have the bug -] - -# Mark the current (or specified) revision as bad -export extern "git bisect bad" [ -] - -# Mark the current (or specified) revision as good -export extern "git bisect good" [ -] - -# Skip the current (or specified) revision -export extern "git bisect skip" [ -] - -# End bisection -export extern "git bisect reset" [ -] - -# Show help for a git subcommand -export extern "git help" [ - command: string@"nu-complete git subcommands" # subcommand to show help for -] diff --git a/nu-git-manager/src/sugar/completions/mod.nu b/nu-git-manager/src/sugar/completions/mod.nu deleted file mode 100644 index e69de29b..00000000 diff --git a/nu-git-manager/src/sugar/dotfiles.nu b/nu-git-manager/src/sugar/dotfiles.nu deleted file mode 100644 index e9c3e5be..00000000 --- a/nu-git-manager/src/sugar/dotfiles.nu +++ /dev/null @@ -1,28 +0,0 @@ -# choose a config file to edit with fuzzy finding -# -# `dotfiles edit` requires the following environment variables to be defined: -# - `$env.DOTFILES_GIT_DIR`: the path to the *bare* repository -# - `$env.DOTFILES_WORKTREE`: the path to the worktree, e.g. `$env.HOME` -# - `$env.EDITOR`: will default to `vim` -# -# this command will `cd` into the directory where the chosen file is to allow -# easier editing and will use the `EDITOR`. -export def-env edit [] { - let choice = ( - git --git-dir $env.DOTFILES_GIT_DIR --work-tree $env.DOTFILES_WORKTREE - lf ~ --full-name - | lines - | input list --fuzzy - $"Please (ansi yellow_italic)choose a config(ansi reset) file to (ansi blue_underline)edit(ansi reset): " - | into string - ) - if ($choice | is-empty) { - return - } - - let path = ($env.HOME | path join $choice) - - cd ($path | path dirname) - ^($env.EDITOR | default "vim") ($path | path basename) - cd - -} diff --git a/nu-git-manager/src/sugar/gh.nu b/nu-git-manager/src/sugar/gh.nu deleted file mode 100644 index 82726a42..00000000 --- a/nu-git-manager/src/sugar/gh.nu +++ /dev/null @@ -1,227 +0,0 @@ -def check-gh-logged-in [] { - let out = (do -i { gh auth status } | complete) - if $out.exit_code != 0 { - error make --unspanned { - msg: $out.stderr - } - } -} - -def "nu-complete list-repos" [context: string] { - let user = ($context | str replace --regex 'gh\s*pr\s*open\s*' "" | split row " " | get 0) - - http get ({ - scheme: https, - username: "", - host: api.github.com, - path: $"/orgs/($user)/repos", - params: { - sort: updated, - per_page: 100, - page: 1 - } - } | url join) - | select name description - | rename value description -} - -def "nu-complete gh-status" [] {[ - [value description]; - - [failure "The CI does not pass."] - [pending "The CI is currently running."] - [success "All the CI jobs have passed."] -]} - -def "nu-complete gh-review" [] {[ - [value description]; - - [none "No review at all."] - [changes-requested "There are changes to be applied."] - [approved "The PR has been approved."] -]} - -export def "pr open" [ - owner: string - repo: string@"nu-complete list-repos" - --draft - --ready # has precedence over `--draft` - --status: string@"nu-complete gh-status" - --review: string@"nu-complete gh-review" -] { - let draft = (if $draft or $ready { - $draft and (not $ready) - }) - - let query = [ - [is pr] - [is open] - [draft $draft] - [status $status] - [review $review] - ] - - let url = ({ - scheme: https, - host: github.com, - path: $"/($owner)/($repo)/pulls", - params: { - q: ( - $query - | where {|it| $it.1 != null} - | each { str join "%3A" } - | str join "+" - ) - } - } | url join) - - xdg-open $url -} - -def unpack-pages [] { - sd -s "}][{" "},{" -} - -def pull [ - endpoint: string -] { - check-gh-logged-in - - gh api --paginate $endpoint # get all the raw data - | unpack-pages # split the pages into a single one - | from json # convert to JSON internally -} - -export def "me notifications" [] { - pull /notifications - | select reason subject.title subject.url - | rename reason title url - | update url {|notification| - $notification | get url | url parse - | update host "github.com" - | update path {|it| - $it.path | str replace --regex "/repos/" "" | str replace --regex "pulls" "pull" - } - | reject params - | url join - } -} - -export def "me issues" [] { - pull /issues -} - -export def "me starred" [ - --reduce (-r) -] { - if ($reduce) { - pull /user/starred - | select -i id name description owner.login clone_url fork license.name created_at pushed_at homepage archived topics size stargazers_count language - } else { - pull /user/starred - } -} - -export def "me repos" [ - owner: string - --user (-u) -] { - let root = if ($user) { "users" } else { "orgs" } - pull $"/($root)/($owner)/repos" -} - -export def "me protection" [ - owner: string - repo: string - branch: string -] { - pull (["" "repos" $owner $repo "branches" $branch "protection"] | str join "/") -} - -export def down [ - project: string -] { - http get (["https://api.github.com/repos" $project "releases"] | path join) - | get assets - | flatten - | select name download_count created_at - | update created_at {|r| $r.created_at | into datetime | date format '%m/%d/%Y %H:%M:%S'} -} - -export def "me pr" [ - number?: int - --open-in-browser (-o) - --force (-f) -] { - check-gh-logged-in - - let repo = ( - gh repo view --json nameWithOwner - | from json - | try { get nameWithOwner } catch { return } - ) - - if not ($number | is-empty) { - if $open_in_browser { - xdg-open ({ - scheme: "https" - host: "github.com" - path: ($repo | path join "pull" ($number | into string)) - } | url join) - } else { - if $force { - gh pr checkout --force $number - } else { - gh pr checkout $number - } - } - return - } - - print $"pulling list of PRs for ($repo)..." - let prs = ( - gh pr list --json title,author,number,createdAt,isDraft,body,url --limit 1000000000 - | from json - | select number title author.login createdAt isDraft body url - | rename id title author date draft body url - | into datetime date - | sort-by date --reverse - ) - - if ($prs | is-empty) { - print $"no PR found for project ($repo)!" - return - } - - let choice = ( - $prs | each {|pr| - [ - $pr.id - $pr.title - $pr.author - $pr.date - $pr.draft - # ($pr.body | str replace --regex --all '\n' "") - $pr.url - ] - | str join " - " - } - | to text - | fzf - | str trim - | split column " - " id title author date draft url - | get 0 - ) - - if ($choice | is-empty) { - return - } - - if $open_in_browser { - xdg-open $choice.url - return - } - - print $"checking out onto PR ($choice.id) from ($choice.author)..." - gh pr checkout $choice.id -} diff --git a/nu-git-manager/src/sugar/gist.nu b/nu-git-manager/src/sugar/gist.nu deleted file mode 100644 index 2549407a..00000000 --- a/nu-git-manager/src/sugar/gist.nu +++ /dev/null @@ -1,137 +0,0 @@ -# give `~/.gists/` as a default value for the GIST home -def default-gist-home [] { - default ($env.HOME | path join ".gists") -} - -# get the true GIST home, possibly with default value -def gist-home [] { - $env.GIST_HOME? | default-gist-home -} - -# list the first 100 gists of a given user -# -# FIXME: related to https://github.com/nushell/nushell/pull/9769 -# returns a -# ``` -# table< -# id: string -# description: string -# files: record -# updated_at: string -# url: string -# > -def list-gists [user: string] { - http get ({ - scheme: https, - username: "", - password: "", - host: api.github.com, - port: "", - path: $"/users/($user)/gists", - fragment: "", - params: { - sort: updated, - per_page: 100, - page: 1 - } - } | url join) - | select id description files updated_at url -} - -# give the lists of known users -def "nu-complete list-known-users" [] { - $env.KNOWN_GITHUB_USERS? | default [] -} - -# list all locally stored gists, i.e. directories under `GIST_HOME` containing a `.git/` -def list-local-gists [] { - ls (gist-home | path join "**" "*" ".git") -} - -# list the gists of a *GitHub* user or all the gists stored locally -export def list [ - user?: string@"nu-complete list-known-users" # the *GitHub* to list the repositories of - --local # only list gists stored locally -] { - if $local { return ( - try { - list-local-gists - } catch { return "no local gist found" } - | get name - | each {|| - path parse | get parent | split row (char path_sep) | last 2 | str join ":" - } - | parse "{user}:{gist}" - )} - - if ($user | is-empty) { - let span = (metadata $user | get span) - error make { - msg: $"(ansi red)gist::no_user_given(ansi reset)" - label: { - text: "no user given" - start: $span.start - end: $span.end - } - } - } - - list-gists $user - | update files {|| get files | transpose | get column1} - | reject files.raw_url -} - -# list the gists of a user for completion -# -# the name of the user is extracted from the context of the command -def "nu-complete list-gists" [context: string] { - let user = ($context | str replace --regex 'gist\s*clone\s*' "" | split row " " | get 0) - list-gists $user | select id description | rename value -} - -# clone a gist of a *GitHub* user into the local `GIT_HOME` -export def clone [ - user: string@"nu-complete list-known-users" # a *GitHub* user to clone a gist from - gist: string@"nu-complete list-gists" # the gist ID to clone -] { - git clone ({ - scheme: https, - host: gists.github.com, - path: $"/($user)/($gist)", - } | url join) ( - gist-home | path join $user $gist - ) -} - -# list all local gist in a completion-friendly format -def "nu-complete list-local-gists" [] { - ls (gist-home | path join "**" "*" ".git") - | update name {|| get name | path dirname} - | upsert description {|it| try { - $it.name | path join "README.md" | open | lines | first 1 | get 0 - }} - | select name description - | update name {|| - get name | str replace --regex (gist-home) "" | str trim -c (char path_sep) - } - | rename value -} - -# jump to a gist in the `GIST_HOME` -export def-env goto [ - gist: string@"nu-complete list-local-gists" # the gist to jump to -] { - cd (gist-home | path join $gist) -} - -# *GitHub* gists -# -# > :bulb: **Note** -# > this module uses the `GIST_HOME` environment variable and defaults its value -# > to `~/.gists/`. -# -# > :bulb: **Note** -# > the `gist` module also uses the `KNOWN_GITHUB_USERS` to propose some known -# > users in completion. -# > this list defaults to the empty list. -export def main [] { help gist } diff --git a/nu-git-manager/src/sugar/git.nu b/nu-git-manager/src/sugar/git.nu deleted file mode 100644 index 2ef8b78b..00000000 --- a/nu-git-manager/src/sugar/git.nu +++ /dev/null @@ -1,202 +0,0 @@ -use std log - -# get a summary of all the operations made between `main` and `HEAD` -export def operations [] { - git log $"(git merge-base FETCH_HEAD main)..HEAD" -M5 --summary - | rg -e 'rename.*=>|delete mode' - | lines - | str trim - | parse '{operation} {file}' - | sort-by operation -} - -# get the commit hash of any revision -export def "get commit" [ - revision: string = "HEAD" # the revision to get the hash of (defaults to "HEAD") -] { - git rev-parse $revision | str trim -} - -# compare two revisions in a `git` repository -export def compare [ - with: string # the target revision to compare the base with - from: string = "HEAD" # the base revision of the comparison (defaults to "HEAD") - --share # output the comparision in pretty shareable format -] { - let start = (git rev-parse $with | str trim) - let end = (git rev-parse $from | str trim) - - if $share { - return $"[`($start)`..`($end)`]\(($start)..($end)\)" - } - - print $"comparing ($start) (char lparen)($with)(char rparen) and ($end) (char lparen)($from)(char rparen)" - git diff $start $end -} - -def repo-root [] { - git rev-parse --show-toplevel | str trim -} - -# removes the index lock -# -# sometimes `git` won't want to run a command because of the `.git/index.lock` file not being -# cleared... -# this command simply removes the lock for you. -export def "lock clean" [] { - try { - rm --verbose (repo-root | path join ".git" "index.lock") - } catch { - print "the index is not busy for now." - } -} - -# go to the root of the repository from anywhere in the worktree -export def-env root [] { - cd (repo-root) -} - -# inspect local branches -# -# without any options, `git branches` will show all dangling branches, i.e. -# local branches that do not have a remote counterpart. -export def branches [ - --report # will give a table report of all the - --clean # clean all dangling branches -] { - let local_branches = (git branch --list | lines | str replace --regex '..' "") - let remote_branches = (git branch -r | lines | str trim | find --invert "HEAD ->" | parse "{remote}/{branch}") - - let branches_report = ( - $local_branches | each {|branch| - { - branch: $branch - remotes: ($remote_branches | where branch == $branch | get remote) - } - } - ) - - if $report { - return $branches_report - } - - let dangling_branches = ($branches_report | where remotes == [] | get branch) - - if ($dangling_branches | length) == 0 { - print "no dangling branch" - return - } - - if $clean { - $dangling_branches | each {|| git branch --delete --force $in} - } else { - $dangling_branches - } -} - -# return true iif the first revision is an ancestor of the second -export def is-ancestor [ - a: string # the base commit-ish revision - b: string # the *head* commit-ish revision -] { - let exit_code = (do -i { - git merge-base $a $b --is-ancestor - } | complete | get exit_code) - - $exit_code == 0 -} - -# get the list of all the remotes in the current repository -export def "remote list" [] { - ^git remote --verbose - | detect columns --no-headers - | rename remote url mode - | str trim - | group-by remote - | transpose - | update column1 { reject remote | select mode url | transpose -r | into record } - | flatten - | rename remote fetch push -} - -# add a new remote to the repository -export def "remote add" [ - name: string # the name of the remote, e.g. `amtoine` - repo: string # the name of the upstream repo, e.g. `nu-git-manager` - host: string # the host where the upstream repo is stored, e.g. `github.com` - --ssh # use SSH as the communication protocol -] { - if $name in (remote list | get remote) { - let span = (metadata $name | get span) - error make { - msg: $"(ansi red_bold)remote_already_in_index(ansi reset)" - label: { - text: $"already a remote of ($env.PWD)" - start: $span.start - end: $span.end - } - } - } - - let url = if $ssh { - $"git@($host):($name)/($repo)" - } else { - $"https://($host)/($name)/($repo)" - } - - ^git remote add $name $url - - remote list | each {|it| - if $it.remote == $name { - $it | transpose | update column1 { $"(ansi yellow_bold)($in)(ansi reset)" } | transpose -r | into record - } else { $it } - } -} - -def "nu-complete remotes" [] { - remote list | get remote -} - -# remove a remote from the local repository -export def "remote remove" [ - ...remotes: string@"nu-complete remotes" # a *rest* list of remotes -] { - let report = ( - remote list | each {|it| - if $it.remote in $remotes { - $it | transpose | update column1 { $"(ansi red_bold)($in)(ansi reset)" } | transpose -r | into record - } else { $it } - } - ) - - $remotes | each {|remote| - if not ($remote in (remote list | get remote)) { - log warning $"($remote) is not a remote of ($env.PWD)" - } else { - log info $"removing ($remote) from ($env.PWD)" - ^git remote remove $remote - } - } | ignore - - $report -} - -# fixup a revision that's not the latest commit -export def fixup [ - revision: string # the revision of the Git worktree to fixup -] { - if (do --ignore-errors { git rev-parse $revision } | complete | get exit_code) != 0 { - let span = (metadata $revision | get span) - error make { - msg: $"(ansi red_bold)revision_not_found(ansi reset)" - label: { - text: $"($revision) not found in the working tree of ($env.PWD)" - start: $span.start - end: $span.end - } - } - } - - git commit --fixup $revision - git rebase --interactive --autosquash $"($revision)~1" -} diff --git a/nu-git-manager/src/sugar/mod.nu b/nu-git-manager/src/sugar/mod.nu deleted file mode 100644 index 38662f45..00000000 --- a/nu-git-manager/src/sugar/mod.nu +++ /dev/null @@ -1 +0,0 @@ -export use completions diff --git a/toolkit/mod.nu b/toolkit/mod.nu deleted file mode 100644 index 0075407d..00000000 --- a/toolkit/mod.nu +++ /dev/null @@ -1,43 +0,0 @@ -def pretty-cmd [] { - let cmd = $in - $"(ansi -e {fg: default attr: di})($cmd)(ansi reset)" - -} - -def "nu-complete import-targets" [] { - ["neovim"] -} - -export def "import" [--into: string@"nu-complete import-targets"] { - if ($into == null) { - print $"(ansi red_italic)--into is a required argument.(ansi reset)" - return - } - - # TODO: replace projects with a config value. - let projects = (match $into { - "neovim" => { $env.HOME | path join ".local" "share" "nvim" "project_nvim" "project_history" }, - _ => { - print $"(ansi red) '(ansi red_italic)($into)' is not a valid target.(ansi reset)" - return - }, - }) - - mkdir ($projects | path dirname) - touch $projects - - let before = ($projects | open | lines | length) - - $projects | open | lines | append ( - ghq list # FIXME: do not use `ghq` as the main dependency - | lines - | each {|it| - print $"adding (ansi yellow)($it)(ansi reset) to the projects..." - ghq root | str trim | path join $it # FIXME: do not use `ghq` as the main dependency - } - ) | uniq - | save -f $projects - - print $"all ('git' | pretty-cmd) projects (ansi green_bold)successfully imported(ansi reset) into the ($projects | pretty-cmd) list!" - print $"from ($before) to ($projects | open | lines | length) projects." -} From 678fa956cab05c1a50496c90e6bd30a53a5d01ec Mon Sep 17 00:00:00 2001 From: "a.stevan" Date: Mon, 9 Oct 2023 11:22:08 +0200 Subject: [PATCH 02/18] add tool commands --- nu-git-manager/fs/store.nu | 16 ++++++++++++++++ nu-git-manager/git/url.nu | 18 ++++++++++++++++++ nu-git-manager/mod.nu | 2 ++ tests/mod.nu | 25 +++++++++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 nu-git-manager/fs/store.nu create mode 100644 nu-git-manager/git/url.nu create mode 100644 nu-git-manager/mod.nu create mode 100644 tests/mod.nu diff --git a/nu-git-manager/fs/store.nu b/nu-git-manager/fs/store.nu new file mode 100644 index 00000000..890128e0 --- /dev/null +++ b/nu-git-manager/fs/store.nu @@ -0,0 +1,16 @@ +export def get-repo-store-path []: nothing -> path { + $env.GIT_REPOS_HOME? | default ( + $env.XDG_DATA_HOME? | default ($nu.home-path | path join ".local/share") | path join "repos" + ) +} + +export def list-repos-in-store []: nothing -> list { + if not (get-repo-store-path | path exists) { + return [] + } + + # FIXME: do not use external `find` command + ^find (get-repo-store-path) -name ".git" + | lines + | each { path split | range 0..(-2) | path join } +} diff --git a/nu-git-manager/git/url.nu b/nu-git-manager/git/url.nu new file mode 100644 index 00000000..72fdd187 --- /dev/null +++ b/nu-git-manager/git/url.nu @@ -0,0 +1,18 @@ +export def parse-git-url []: string -> record { + str replace --regex '^git@(.*):' 'ssh://$1/' + | str replace --regex '\.git$' '' + | url parse + | select host path + | update path { + str trim --left --right --char '/' + | str replace --regex '\/tree\/.*' '' + | path split + | { + owner: ($in | first), + group: ($in | range 1..(-2) | if $in != null { path join }), + repo: ($in | last) + } + } + | flatten + | into record +} diff --git a/nu-git-manager/mod.nu b/nu-git-manager/mod.nu new file mode 100644 index 00000000..2f25662e --- /dev/null +++ b/nu-git-manager/mod.nu @@ -0,0 +1,2 @@ +use fs/store.nu [get-repo-store-path, list-repos-in-store] +use git/url.nu parse-git-url diff --git a/tests/mod.nu b/tests/mod.nu new file mode 100644 index 00000000..461ecff8 --- /dev/null +++ b/tests/mod.nu @@ -0,0 +1,25 @@ +use ../nu-git-manager/git/url.nu parse-git-url + +export def git-url-parsing [] { + use std assert + + let cases = [ + [input, host, owner, group, repo]; + + ["https://github.com/foo/bar", "github.com", "foo", null, "bar"], + ["https://github.com/foo/bar.git", "github.com", "foo", null, "bar"], + ["https://github.com/foo/bar/tree/branch/file", "github.com", "foo", null, "bar"], + ["ssh://github.com/foo/bar", "github.com", "foo", null, "bar"], + ["git@github.com:foo/bar", "github.com", "foo", null, "bar"], + ["https://gitlab.com/foo/bar", "gitlab.com", "foo", null, "bar"], + ["git@gitlab.com:foo/bar", "gitlab.com", "foo", null, "bar"], + ["git@gitlab.com:foo/bar/baz/brr", "gitlab.com", "foo", "bar/baz", "brr"], + ] + + for case in $cases { + let expected = { + host: $case.host, owner: $case.owner, group: $case.group, repo: $case.repo + } + assert equal ($case.input | parse-git-url) $expected + } +} From 7b52fa669c6fd8e34abe2770b0b98ff0b83c87b1 Mon Sep 17 00:00:00 2001 From: "a.stevan" Date: Mon, 9 Oct 2023 11:44:17 +0200 Subject: [PATCH 03/18] add gm --- nu-git-manager/git/url.nu | 41 +++++++++ nu-git-manager/mod.nu | 173 +++++++++++++++++++++++++++++++++++++- tests/mod.nu | 22 ++++- 3 files changed, 232 insertions(+), 4 deletions(-) diff --git a/nu-git-manager/git/url.nu b/nu-git-manager/git/url.nu index 72fdd187..09d3191d 100644 --- a/nu-git-manager/git/url.nu +++ b/nu-git-manager/git/url.nu @@ -16,3 +16,44 @@ export def parse-git-url []: string -> record, # typically from `parse-git-url` + fetch: string, # one of 'https', 'ssh', or empty + push: string, # one of 'https', 'ssh', or empty + ssh: bool, +]: nothing -> record { + let base_url = { + scheme: null, + host: $repository.host, + path: ([$repository.owner $repository.group $repository.repo] | compact | path join) + } + let http_url = $base_url | update scheme "https" | url join + let ssh_url = $base_url | update scheme "ssh" | url join + + let fetch_url = match $fetch { + "https" => $http_url, + "ssh" => $ssh_url, + _ => { + if $ssh { + $ssh_url + } else { + $http_url + } + }, + } + + let push_url = match $push { + "https" => $http_url, + "ssh" => $ssh_url, + _ => { + if $ssh { + $ssh_url + } else { + $http_url + } + }, + } + + {fetch: $fetch_url, push: $push_url} +} diff --git a/nu-git-manager/mod.nu b/nu-git-manager/mod.nu index 2f25662e..61fdb3e0 100644 --- a/nu-git-manager/mod.nu +++ b/nu-git-manager/mod.nu @@ -1,2 +1,173 @@ +use std log + use fs/store.nu [get-repo-store-path, list-repos-in-store] -use git/url.nu parse-git-url +use git/url.nu [parse-git-url, get-fetch-push-urls] + +def "nu-complete git-protocols" []: nothing -> table { + [ + [value, description]; + + ["https", "use the HTTP protocol: will require a PAT authentification for private repositories"], + ["ssh", "use the SSH protocol: will require a passphrase unless setup otherwise"], + ] +} + +# manage your Git repositories with the main command of `nu-git-manager` +export def "gm" []: nothing -> nothing { + print (help gm) +} + +# clone a remote Git repository into your local store +# +# will give a nice error if the repository is already in the local store. +# +# # Examples +# clone a repository in the local store of `nu-git-manager` +# > gm clone https://github.com/amtoine/nu-git-manager +# +# clone as a bare repository, i.e. a repo without a worktree +# > gm clone --bare https://github.com/amtoine/nu-git-manager +# +# clone a repo and change the name of the remote +# > gm clone --remote default https://github.com/amtoine/nu-git-manager +# +# setup a public repo in the local store and use HTTP to fetch without PAT and push with SSH +# > gm clone https://github.com/amtoine/nu-git-manager --fetch https --push ssh +export def "gm clone" [ + url: string # the URL to the repository to clone, supports HTTPS and SSH links, as well as references ending in `.git` or starting with `git@` + --remote: string = "origin" # the name of the remote to setup + --ssh # setup the remote to use the SSH protocol both to FETCH and to PUSH + --fetch: string@"nu-complete git-protocols" # setup the FETCH protocol explicitely, will overwrite `--ssh` for FETCH + --push: string@"nu-complete git-protocols" # setup the PUSH protocol explicitely, will overwrite `--ssh` for PUSH + --bare # clone the repository as a "bare" project +]: nothing -> nothing { + let repository = $url | parse-git-url + + let local_path = get-repo-store-path + | append [$repository.host $repository.owner $repository.group $repository.repo] + | compact + | path join + + if ($local_path | path exists) { + let span = metadata $url | get span + error make { + msg: $"(ansi red_bold)repository_already_in_store(ansi reset)" + label: { + text: $"this repository has already been cloned by (ansi {fg: "default_dimmed", attr: "it"})gm(ansi reset)" + start: $span.start + end: $span.end + } + } + } + + let urls = get-fetch-push-urls $repository $fetch $push $ssh + + if $bare { + git clone $urls.fetch $local_path --origin $remote --bare + } else { + git clone $urls.fetch $local_path --origin $remote + } + + git -C $local_path remote set-url $remote $urls.fetch + git -C $local_path remote set-url $remote --push $urls.push +} + +# list all the local repositories in your local store +# +# # Examples +# list all the repositories in the store +# > gm list +# +# list all the repositories in the store with their full paths +# > gm list --full-path +# +# jump to a directory in the store +# > cd (gm list --full-path | input list) +export def "gm list" [ + --full-path # show the full path instead of only the "owner + group + repo" name +]: nothing -> list { + if $full_path { + list-repos-in-store + } else { + let root = get-repo-store-path + list-repos-in-store | each { + str replace $root '' | str trim --left --char (char path_sep) + } + } +} + +# get the root of the local store of repositories managed by `nu-git-manager` +# +# `nu-git-manager` will look for a store in the following places, in order: +# - `$env.GIT_REPOS_HOME` +# - `$env.XDG_DATA_HOME | path join "repos" +# - `~/.local/share/repos` +# +# # Example +# a contrived example +# > GIT_REPOS_HOME=foo gm root +# foo +export def "gm root" []: nothing -> path { + get-repo-store-path +} + +# remove one of the repositories from your local store +# +# # Examples +# remove any repository by fuzzy-finding the whole store +# > gm remove --fuzzy +# +# restrict the search to any one of my repositories +# > gm remove amtoine +# +# remove a precise repo by giving its full name, a name collision is unlikely +# > gm remove amtoine/nu-git-manager +export def "gm remove" [ + pattern?: string # a pattern to restrict the choices + --fuzzy # remove after fuzzy-finding the repo(s) to clean +]: nothing -> nothing { + let root = get-repo-store-path + let choices = list-repos-in-store + | each { + str replace $root '' | str trim --left --char (char path_sep) + } + | find $pattern + + let repo_to_remove = match ($choices | length) { + 0 => { + let span = metadata $pattern | get span + error make { + msg: $"(ansi red_bold)no_matching_repository(ansi reset)" + label: { + text: $"no repository matching this in (ansi {fg: "default_dimmed", attr: "it"})($root)(ansi reset)" + start: $span.start + end: $span.end + } + } + }, + 1 => { $choices | first }, + _ => { + let prompt = $"please choose a repository to (ansi red)remove(ansi reset)" + let choice = if $fuzzy { + $choices | input list --fuzzy $prompt + } else { + $choices | input list $prompt + } + + if ($choice | is-empty) { + log info "user chose to exit" + return + } + + $choice + }, + } + + let prompt = $"are you (ansi defu)sure(ansi reset) you want to (ansi red_bold)remove(ansi reset) (ansi yellow)($repo_to_remove)(ansi reset)? " + match (["no", "yes"] | input list $prompt) { + "no" => { log info $"user chose to (ansi green_bold)keep(ansi reset) (ansi yellow)($repo_to_remove)(ansi reset)" }, + "yes" => { rm --recursive --force --verbose ($root | path join $repo_to_remove) }, + } + + null +} diff --git a/tests/mod.nu b/tests/mod.nu index 461ecff8..503cce42 100644 --- a/tests/mod.nu +++ b/tests/mod.nu @@ -1,8 +1,8 @@ -use ../nu-git-manager/git/url.nu parse-git-url +use std assert -export def git-url-parsing [] { - use std assert +use ../nu-git-manager/git/url.nu [parse-git-url, get-fetch-push-urls] +export def git-url-parsing [] { let cases = [ [input, host, owner, group, repo]; @@ -23,3 +23,19 @@ export def git-url-parsing [] { assert equal ($case.input | parse-git-url) $expected } } + +export def fetch-and-push-urls [] { + let cases = [ + [host, owner, group, repo, fetch_protocol, push_protocol, use_ssh, fetch_url, push_url]; + ["host", "foo", "", "bar", "", "", false, "https://host/foo/bar", "https://host/foo/bar"], + ["host", "foo", "", "bar", "", "", false, "https://host/foo/bar", "https://host/foo/bar"], + ] + + for case in $cases { + let repo = {host: $case.host, owner: $case.owner, group: $case.group, repo: $case.repo} + + let actual = get-fetch-push-urls $repo $case.fetch_protocol $case.push_protocol $case.use_ssh + let expected = {fetch: $case.fetch_url, push: $case.push_url} + assert equal $actual $expected + } +} From a355a358179310fb50c367958c7d69bc9f6b62b1 Mon Sep 17 00:00:00 2001 From: "a.stevan" Date: Mon, 9 Oct 2023 16:05:55 +0200 Subject: [PATCH 04/18] put back the *sugar* modules --- nu-git-manager/sugar/gh.nu | 227 +++++++++++++++++++++++++++++++++++ nu-git-manager/sugar/gist.nu | 137 +++++++++++++++++++++ nu-git-manager/sugar/git.nu | 202 +++++++++++++++++++++++++++++++ nu-git-manager/sugar/mod.nu | 0 4 files changed, 566 insertions(+) create mode 100644 nu-git-manager/sugar/gh.nu create mode 100644 nu-git-manager/sugar/gist.nu create mode 100644 nu-git-manager/sugar/git.nu create mode 100644 nu-git-manager/sugar/mod.nu diff --git a/nu-git-manager/sugar/gh.nu b/nu-git-manager/sugar/gh.nu new file mode 100644 index 00000000..82726a42 --- /dev/null +++ b/nu-git-manager/sugar/gh.nu @@ -0,0 +1,227 @@ +def check-gh-logged-in [] { + let out = (do -i { gh auth status } | complete) + if $out.exit_code != 0 { + error make --unspanned { + msg: $out.stderr + } + } +} + +def "nu-complete list-repos" [context: string] { + let user = ($context | str replace --regex 'gh\s*pr\s*open\s*' "" | split row " " | get 0) + + http get ({ + scheme: https, + username: "", + host: api.github.com, + path: $"/orgs/($user)/repos", + params: { + sort: updated, + per_page: 100, + page: 1 + } + } | url join) + | select name description + | rename value description +} + +def "nu-complete gh-status" [] {[ + [value description]; + + [failure "The CI does not pass."] + [pending "The CI is currently running."] + [success "All the CI jobs have passed."] +]} + +def "nu-complete gh-review" [] {[ + [value description]; + + [none "No review at all."] + [changes-requested "There are changes to be applied."] + [approved "The PR has been approved."] +]} + +export def "pr open" [ + owner: string + repo: string@"nu-complete list-repos" + --draft + --ready # has precedence over `--draft` + --status: string@"nu-complete gh-status" + --review: string@"nu-complete gh-review" +] { + let draft = (if $draft or $ready { + $draft and (not $ready) + }) + + let query = [ + [is pr] + [is open] + [draft $draft] + [status $status] + [review $review] + ] + + let url = ({ + scheme: https, + host: github.com, + path: $"/($owner)/($repo)/pulls", + params: { + q: ( + $query + | where {|it| $it.1 != null} + | each { str join "%3A" } + | str join "+" + ) + } + } | url join) + + xdg-open $url +} + +def unpack-pages [] { + sd -s "}][{" "},{" +} + +def pull [ + endpoint: string +] { + check-gh-logged-in + + gh api --paginate $endpoint # get all the raw data + | unpack-pages # split the pages into a single one + | from json # convert to JSON internally +} + +export def "me notifications" [] { + pull /notifications + | select reason subject.title subject.url + | rename reason title url + | update url {|notification| + $notification | get url | url parse + | update host "github.com" + | update path {|it| + $it.path | str replace --regex "/repos/" "" | str replace --regex "pulls" "pull" + } + | reject params + | url join + } +} + +export def "me issues" [] { + pull /issues +} + +export def "me starred" [ + --reduce (-r) +] { + if ($reduce) { + pull /user/starred + | select -i id name description owner.login clone_url fork license.name created_at pushed_at homepage archived topics size stargazers_count language + } else { + pull /user/starred + } +} + +export def "me repos" [ + owner: string + --user (-u) +] { + let root = if ($user) { "users" } else { "orgs" } + pull $"/($root)/($owner)/repos" +} + +export def "me protection" [ + owner: string + repo: string + branch: string +] { + pull (["" "repos" $owner $repo "branches" $branch "protection"] | str join "/") +} + +export def down [ + project: string +] { + http get (["https://api.github.com/repos" $project "releases"] | path join) + | get assets + | flatten + | select name download_count created_at + | update created_at {|r| $r.created_at | into datetime | date format '%m/%d/%Y %H:%M:%S'} +} + +export def "me pr" [ + number?: int + --open-in-browser (-o) + --force (-f) +] { + check-gh-logged-in + + let repo = ( + gh repo view --json nameWithOwner + | from json + | try { get nameWithOwner } catch { return } + ) + + if not ($number | is-empty) { + if $open_in_browser { + xdg-open ({ + scheme: "https" + host: "github.com" + path: ($repo | path join "pull" ($number | into string)) + } | url join) + } else { + if $force { + gh pr checkout --force $number + } else { + gh pr checkout $number + } + } + return + } + + print $"pulling list of PRs for ($repo)..." + let prs = ( + gh pr list --json title,author,number,createdAt,isDraft,body,url --limit 1000000000 + | from json + | select number title author.login createdAt isDraft body url + | rename id title author date draft body url + | into datetime date + | sort-by date --reverse + ) + + if ($prs | is-empty) { + print $"no PR found for project ($repo)!" + return + } + + let choice = ( + $prs | each {|pr| + [ + $pr.id + $pr.title + $pr.author + $pr.date + $pr.draft + # ($pr.body | str replace --regex --all '\n' "") + $pr.url + ] + | str join " - " + } + | to text + | fzf + | str trim + | split column " - " id title author date draft url + | get 0 + ) + + if ($choice | is-empty) { + return + } + + if $open_in_browser { + xdg-open $choice.url + return + } + + print $"checking out onto PR ($choice.id) from ($choice.author)..." + gh pr checkout $choice.id +} diff --git a/nu-git-manager/sugar/gist.nu b/nu-git-manager/sugar/gist.nu new file mode 100644 index 00000000..2549407a --- /dev/null +++ b/nu-git-manager/sugar/gist.nu @@ -0,0 +1,137 @@ +# give `~/.gists/` as a default value for the GIST home +def default-gist-home [] { + default ($env.HOME | path join ".gists") +} + +# get the true GIST home, possibly with default value +def gist-home [] { + $env.GIST_HOME? | default-gist-home +} + +# list the first 100 gists of a given user +# +# FIXME: related to https://github.com/nushell/nushell/pull/9769 +# returns a +# ``` +# table< +# id: string +# description: string +# files: record +# updated_at: string +# url: string +# > +def list-gists [user: string] { + http get ({ + scheme: https, + username: "", + password: "", + host: api.github.com, + port: "", + path: $"/users/($user)/gists", + fragment: "", + params: { + sort: updated, + per_page: 100, + page: 1 + } + } | url join) + | select id description files updated_at url +} + +# give the lists of known users +def "nu-complete list-known-users" [] { + $env.KNOWN_GITHUB_USERS? | default [] +} + +# list all locally stored gists, i.e. directories under `GIST_HOME` containing a `.git/` +def list-local-gists [] { + ls (gist-home | path join "**" "*" ".git") +} + +# list the gists of a *GitHub* user or all the gists stored locally +export def list [ + user?: string@"nu-complete list-known-users" # the *GitHub* to list the repositories of + --local # only list gists stored locally +] { + if $local { return ( + try { + list-local-gists + } catch { return "no local gist found" } + | get name + | each {|| + path parse | get parent | split row (char path_sep) | last 2 | str join ":" + } + | parse "{user}:{gist}" + )} + + if ($user | is-empty) { + let span = (metadata $user | get span) + error make { + msg: $"(ansi red)gist::no_user_given(ansi reset)" + label: { + text: "no user given" + start: $span.start + end: $span.end + } + } + } + + list-gists $user + | update files {|| get files | transpose | get column1} + | reject files.raw_url +} + +# list the gists of a user for completion +# +# the name of the user is extracted from the context of the command +def "nu-complete list-gists" [context: string] { + let user = ($context | str replace --regex 'gist\s*clone\s*' "" | split row " " | get 0) + list-gists $user | select id description | rename value +} + +# clone a gist of a *GitHub* user into the local `GIT_HOME` +export def clone [ + user: string@"nu-complete list-known-users" # a *GitHub* user to clone a gist from + gist: string@"nu-complete list-gists" # the gist ID to clone +] { + git clone ({ + scheme: https, + host: gists.github.com, + path: $"/($user)/($gist)", + } | url join) ( + gist-home | path join $user $gist + ) +} + +# list all local gist in a completion-friendly format +def "nu-complete list-local-gists" [] { + ls (gist-home | path join "**" "*" ".git") + | update name {|| get name | path dirname} + | upsert description {|it| try { + $it.name | path join "README.md" | open | lines | first 1 | get 0 + }} + | select name description + | update name {|| + get name | str replace --regex (gist-home) "" | str trim -c (char path_sep) + } + | rename value +} + +# jump to a gist in the `GIST_HOME` +export def-env goto [ + gist: string@"nu-complete list-local-gists" # the gist to jump to +] { + cd (gist-home | path join $gist) +} + +# *GitHub* gists +# +# > :bulb: **Note** +# > this module uses the `GIST_HOME` environment variable and defaults its value +# > to `~/.gists/`. +# +# > :bulb: **Note** +# > the `gist` module also uses the `KNOWN_GITHUB_USERS` to propose some known +# > users in completion. +# > this list defaults to the empty list. +export def main [] { help gist } diff --git a/nu-git-manager/sugar/git.nu b/nu-git-manager/sugar/git.nu new file mode 100644 index 00000000..2ef8b78b --- /dev/null +++ b/nu-git-manager/sugar/git.nu @@ -0,0 +1,202 @@ +use std log + +# get a summary of all the operations made between `main` and `HEAD` +export def operations [] { + git log $"(git merge-base FETCH_HEAD main)..HEAD" -M5 --summary + | rg -e 'rename.*=>|delete mode' + | lines + | str trim + | parse '{operation} {file}' + | sort-by operation +} + +# get the commit hash of any revision +export def "get commit" [ + revision: string = "HEAD" # the revision to get the hash of (defaults to "HEAD") +] { + git rev-parse $revision | str trim +} + +# compare two revisions in a `git` repository +export def compare [ + with: string # the target revision to compare the base with + from: string = "HEAD" # the base revision of the comparison (defaults to "HEAD") + --share # output the comparision in pretty shareable format +] { + let start = (git rev-parse $with | str trim) + let end = (git rev-parse $from | str trim) + + if $share { + return $"[`($start)`..`($end)`]\(($start)..($end)\)" + } + + print $"comparing ($start) (char lparen)($with)(char rparen) and ($end) (char lparen)($from)(char rparen)" + git diff $start $end +} + +def repo-root [] { + git rev-parse --show-toplevel | str trim +} + +# removes the index lock +# +# sometimes `git` won't want to run a command because of the `.git/index.lock` file not being +# cleared... +# this command simply removes the lock for you. +export def "lock clean" [] { + try { + rm --verbose (repo-root | path join ".git" "index.lock") + } catch { + print "the index is not busy for now." + } +} + +# go to the root of the repository from anywhere in the worktree +export def-env root [] { + cd (repo-root) +} + +# inspect local branches +# +# without any options, `git branches` will show all dangling branches, i.e. +# local branches that do not have a remote counterpart. +export def branches [ + --report # will give a table report of all the + --clean # clean all dangling branches +] { + let local_branches = (git branch --list | lines | str replace --regex '..' "") + let remote_branches = (git branch -r | lines | str trim | find --invert "HEAD ->" | parse "{remote}/{branch}") + + let branches_report = ( + $local_branches | each {|branch| + { + branch: $branch + remotes: ($remote_branches | where branch == $branch | get remote) + } + } + ) + + if $report { + return $branches_report + } + + let dangling_branches = ($branches_report | where remotes == [] | get branch) + + if ($dangling_branches | length) == 0 { + print "no dangling branch" + return + } + + if $clean { + $dangling_branches | each {|| git branch --delete --force $in} + } else { + $dangling_branches + } +} + +# return true iif the first revision is an ancestor of the second +export def is-ancestor [ + a: string # the base commit-ish revision + b: string # the *head* commit-ish revision +] { + let exit_code = (do -i { + git merge-base $a $b --is-ancestor + } | complete | get exit_code) + + $exit_code == 0 +} + +# get the list of all the remotes in the current repository +export def "remote list" [] { + ^git remote --verbose + | detect columns --no-headers + | rename remote url mode + | str trim + | group-by remote + | transpose + | update column1 { reject remote | select mode url | transpose -r | into record } + | flatten + | rename remote fetch push +} + +# add a new remote to the repository +export def "remote add" [ + name: string # the name of the remote, e.g. `amtoine` + repo: string # the name of the upstream repo, e.g. `nu-git-manager` + host: string # the host where the upstream repo is stored, e.g. `github.com` + --ssh # use SSH as the communication protocol +] { + if $name in (remote list | get remote) { + let span = (metadata $name | get span) + error make { + msg: $"(ansi red_bold)remote_already_in_index(ansi reset)" + label: { + text: $"already a remote of ($env.PWD)" + start: $span.start + end: $span.end + } + } + } + + let url = if $ssh { + $"git@($host):($name)/($repo)" + } else { + $"https://($host)/($name)/($repo)" + } + + ^git remote add $name $url + + remote list | each {|it| + if $it.remote == $name { + $it | transpose | update column1 { $"(ansi yellow_bold)($in)(ansi reset)" } | transpose -r | into record + } else { $it } + } +} + +def "nu-complete remotes" [] { + remote list | get remote +} + +# remove a remote from the local repository +export def "remote remove" [ + ...remotes: string@"nu-complete remotes" # a *rest* list of remotes +] { + let report = ( + remote list | each {|it| + if $it.remote in $remotes { + $it | transpose | update column1 { $"(ansi red_bold)($in)(ansi reset)" } | transpose -r | into record + } else { $it } + } + ) + + $remotes | each {|remote| + if not ($remote in (remote list | get remote)) { + log warning $"($remote) is not a remote of ($env.PWD)" + } else { + log info $"removing ($remote) from ($env.PWD)" + ^git remote remove $remote + } + } | ignore + + $report +} + +# fixup a revision that's not the latest commit +export def fixup [ + revision: string # the revision of the Git worktree to fixup +] { + if (do --ignore-errors { git rev-parse $revision } | complete | get exit_code) != 0 { + let span = (metadata $revision | get span) + error make { + msg: $"(ansi red_bold)revision_not_found(ansi reset)" + label: { + text: $"($revision) not found in the working tree of ($env.PWD)" + start: $span.start + end: $span.end + } + } + } + + git commit --fixup $revision + git rebase --interactive --autosquash $"($revision)~1" +} diff --git a/nu-git-manager/sugar/mod.nu b/nu-git-manager/sugar/mod.nu new file mode 100644 index 00000000..e69de29b From 8e05449d48634798ed93f6a523823607337189a7 Mon Sep 17 00:00:00 2001 From: "a.stevan" Date: Mon, 9 Oct 2023 15:49:56 +0200 Subject: [PATCH 05/18] bump the version to `0.2.0` --- package.nuon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.nuon b/package.nuon index 4655c34c..2fe86d0f 100644 --- a/package.nuon +++ b/package.nuon @@ -1,6 +1,6 @@ { name: "nu-git-manager" - version: 0.1.0 + version: 0.2.0 description: "A collection of Nushell tools to manage `git` repositories." documentation: "https://github.com/amtoine/nu-git-manager/blob/main/README.md" maintainers: [ From aab524c75487faf94c1b6c0801ac9ff736c06383 Mon Sep 17 00:00:00 2001 From: amtoine Date: Tue, 10 Oct 2023 18:26:49 +0200 Subject: [PATCH 06/18] update the README --- README.md | 112 +++++++++++++++++++++++++++--------------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 3515d608..1f611948 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,56 @@ # nu-git-manager A collection of Nushell tools to manage `git` repositories. -## :bulb: what is `nu-git-manager` +# Table of content +- [*what is `nu-git-manager`*](#bulb-what-is-nu-git-manager-toc) +- [*requirements*](#link-requirements-toc) +- [*installation*](#recycle-installation-toc) +- [*usage*](#gear-usage-toc) + - [*getting help*](#pray-getting-help-toc) +- [*some ideas of advanced (?) usage*](#exclamation-some-ideas-of-advanced--usage-toc) + +## :bulb: what is `nu-git-manager` [[toc](#table-of-content)] like [`ghq`](https://github.com/x-motemen/ghq), `nu-git-manager` aims at being a fully-featured repository manager, purely written in Nushell. -the public API of `nu-git-manager` is greatly inspired by `ghq` for now but this might very likely change -in the future! - -regarding versions, as `nu-git-manager` is tied to the version of the main `nushell/nushell` repo, -its versioning cycle will be the same -- a new minor release every 3 weeks -- starting may 2023 tuesday the 16th - -more information can be found in the [documentation](docs/)! +it provides two main modules: +- `nu-git-manager` itself which ships the main `gm` command +- `nu-git-manager sugar` which exports a bunch of Git-related tools, e.g. to help use the `gh` command or augment the capabilities of `git` -## :link: requirements -- Nushell 0.80.1+ -- `git` 2.40.1 +## :link: requirements [[toc](#table-of-content)] +- [Nushell] 0.85.1+ +- `git` 2.34.1 - `gh` (optional) 2.29.0 (used by `sugar gh`) -## :recycle: installation [here](docs/installation/) +## :recycle: installation [[toc](#table-of-content)] +- install [Nupm] (**recommended**) by following the [Nupm instructions] +- download the `nu-git-manager` repository +```shell +git clone https://github.com/amtoine/nu-git-manager +``` +- activate the `nupm` module with `use nupm` +- install the `nu-git-manager` package +```nushell +nupm install --path --force nu-git-manager +``` -## :gear: usage +## :gear: usage [[toc](#table-of-content)] in your `config.nu` you can add the following to load `nu-git-manager` modules: ```nu # config.nu -# load the main `gm` module -use nu-git-manager gm +# load the main `gm` command +use nu-git-manager * # the following are non-essential modules -use nu-git-manager sugar git # load `git` tool extensions +use nu-git-manager sugar git # augmnet Git with custom commands use nu-git-manager sugar gh # load commands to interact with *GitHub* use nu-git-manager sugar gist # load commands to interact with *GitHub* gists -use nu-git-manager sugar completions git * # load some `git` completion -use nu-git-manager sugar dotfiles # load tools to manage versionned dotfiles ``` then you have access to the whole `nu-git-manager` suite :partying_face: -### :pray: getting help +### :pray: getting help [[toc](#table-of-content)] do not hesitate to run one of the following to have more information about what `nu-git-manager` has to offer :thumbsup: ```nu help gm @@ -59,42 +69,32 @@ help modules gist gist ``` -## :exclamation: some ideas of advanced (?) usage -one thing i like to do in my config to go ***BLAZZINGLY FAST*** is to use keybindings to call some `nu-git-manager` commands -in one key stroke :smirk: +## :exclamation: some ideas of advanced (?) usage [[toc](#table-of-content)] +everytime i open a terminal, i use [Tmux] to manage sessions, switch between them, detach and reattach, quite a ***BLAZZINGLY FAST*** workflow for my taste :smirk: -- with `gm` activated, i can jump to any repo from anywhere with `` -```nu -{ - name: open_repo - modifier: control - keycode: char_g - mode: [emacs, vi_insert, vi_normal] - event: { - send: executehostcommand - cmd: "gm goto" - } -} -``` -- with `sugar dotfiles` activated, i can edit any configuration file from anywhere with `` -```nu -{ - name: edit_config - modifier: control - keycode: char_v - mode: [emacs, vi_insert, vi_normal] - event: { - send: executehostcommand - cmd: "dotfiles edit" - } -} +to achieve this, i use the [`tmux-sessionizer.nu` script][`tmux-sessionizer.nu`] from the [`nu-goat-scripts` package][`nu-goat-scripts`], again installed with [Nupm] :ok_hand: + +then, in my Tmux config, i have a binding to +1. list all my Git repositories +2. fuzzy-pick one of them with the [`main` command of `tmux-sessionizer.nu`][`tmux-sessionizer.nu`] +3. create or reattach to the session associated with the repository +```bash +# ~/.config/tmux/tmux.conf + +NUPM_HOME="~/.local/share/nupm" +TMUX_SESSIONIZER="$NUPM_HOME/scripts/tmux-sessionizer.nu" + +bind-key -r t display-popup -E "nu --commands ' + use $NUPM_HOME/modules/nu-git-manager *;\ + $TMUX_SESSIONIZER (gm list --full-path) --short\ +'" ``` -## :calendar: the roadmap of `nu-git-manager` -- [ ] complete the main `gm` commands -- [ ] support more hosts, e.g. *GitLab* -- [ ] add more external completions, namely for `git` and `gh`, maybe `glab` +[Nushell]: https://github.com/nushell/nushell + +[Nupm]: https://github.com/nushell/nupm +[Nupm instructions]: https://github.com/nushell/nupm#-installation -[nushell/nushell#9066]: https://github.com/nushell/nushell/pull/9066 -[`a2a346e39`]: https://github.com/nushell/nushell/commit/a2a346e39c53e386b97d8d7f9a05ed58298e8789 -[#21]: https://github.com/amtoine/nu-git-manager/pull/21 +[Tmux]: https://github.com/tmux/tmux +[`tmux-sessionizer.nu`]: https://github.com/goatfiles/scripts/blob/main/nu_scripts/scripts/tmux-sessionizer.nu#L463 +[`nu-goat-scripts`]: https://github.com/goatfiles/scripts/blob/main/nu_scripts/README.md#nu_scripts From d8b92089ad80ba5abc78fe4b615872f6f9edf0b5 Mon Sep 17 00:00:00 2001 From: amtoine Date: Wed, 11 Oct 2023 17:52:14 +0200 Subject: [PATCH 07/18] fix the import in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f611948..89d45f93 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ in your `config.nu` you can add the following to load `nu-git-manager` modules: # config.nu # load the main `gm` command -use nu-git-manager * +use nu-git-manager [gm, "gm clone", "gm list", "gm root", "gm remove"] # the following are non-essential modules use nu-git-manager sugar git # augmnet Git with custom commands From 5e970e2667fdc4a114aee8c57b3291f63ac51c3b Mon Sep 17 00:00:00 2001 From: amtoine Date: Wed, 11 Oct 2023 23:12:02 +0200 Subject: [PATCH 08/18] complete the tests for `get-fetch-push-urls` --- tests/mod.nu | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/tests/mod.nu b/tests/mod.nu index 503cce42..69bbae79 100644 --- a/tests/mod.nu +++ b/tests/mod.nu @@ -26,16 +26,43 @@ export def git-url-parsing [] { export def fetch-and-push-urls [] { let cases = [ - [host, owner, group, repo, fetch_protocol, push_protocol, use_ssh, fetch_url, push_url]; - ["host", "foo", "", "bar", "", "", false, "https://host/foo/bar", "https://host/foo/bar"], - ["host", "foo", "", "bar", "", "", false, "https://host/foo/bar", "https://host/foo/bar"], + [use_ssh, user_fetch, user_push, fetch_protocol, push_protocol]; + + # - if user_fetch is not-empty: fetch_protocol is the same (same for push) + # - if user_fetch is empty: fetch_protocol is `https` if not `use_ssh` (same for push) + [false, "", "", "https", "https"], + [false, "", "ssh", "https", "ssh"], + [false, "", "https", "https", "https"], + [false, "ssh", "", "ssh", "https"], + [false, "ssh", "ssh", "ssh", "ssh"], + [false, "ssh", "https", "ssh", "https"], + [false, "https", "", "https", "https"], + [false, "https", "ssh", "https", "ssh"], + [false, "https", "https", "https", "https"], + [true, "", "", "ssh", "ssh"], + [true, "", "ssh", "ssh", "ssh"], + [true, "", "https", "ssh", "https"], + [true, "ssh", "", "ssh", "ssh"], + [true, "ssh", "ssh", "ssh", "ssh"], + [true, "ssh", "https", "ssh", "https"], + [true, "https", "", "https", "ssh"], + [true, "https", "ssh", "https", "ssh"], + [true, "https", "https", "https", "https"], ] - for case in $cases { - let repo = {host: $case.host, owner: $case.owner, group: $case.group, repo: $case.repo} + let repo = {host: "h", owner: "o", group: "", repo: "r"} + let base_url = { + scheme: null, + host: $repo.host, + path: ([$repo.owner, $repo.group, $repo.repo] | compact | path join) + } - let actual = get-fetch-push-urls $repo $case.fetch_protocol $case.push_protocol $case.use_ssh - let expected = {fetch: $case.fetch_url, push: $case.push_url} - assert equal $actual $expected + for case in $cases { + let actual = get-fetch-push-urls $repo $case.user_fetch $case.user_push $case.use_ssh + let expected = { + fetch: ($base_url | update scheme $case.fetch_protocol | url join) + push: ($base_url | update scheme $case.push_protocol | url join) + } + assert equal $actual $expected $"input: ($case)" } } From 7cd66bffbabc99441b956d98420a267e37c2ab00 Mon Sep 17 00:00:00 2001 From: amtoine Date: Thu, 12 Oct 2023 19:24:24 +0200 Subject: [PATCH 09/18] add dependencies and ways to install them --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 89d45f93..d045d857 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,16 @@ it provides two main modules: ## :link: requirements [[toc](#table-of-content)] - [Nushell] 0.85.1+ + - with Cargo and `cargo install nu` - `git` 2.34.1 + - with Pacman and `pacman -S extra/git` + - with Nix and `nix run nixpkgs#git` - `gh` (optional) 2.29.0 (used by `sugar gh`) + - with Pacman and `pacman -S community/github-cli` + - with Nix and `nix run nixpkgs#gh` +- `find` 4.9.0 + - with Pacman and `pacman -S core/findutils` + - with Nix and `nix run nixpkgs#findutils` ## :recycle: installation [[toc](#table-of-content)] - install [Nupm] (**recommended**) by following the [Nupm instructions] From c427d59c5b06c8d76fd270890feea52e4e26abe9 Mon Sep 17 00:00:00 2001 From: amtoine Date: Fri, 13 Oct 2023 18:21:42 +0200 Subject: [PATCH 10/18] expand the root path --- nu-git-manager/fs/store.nu | 2 +- nu-git-manager/mod.nu | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nu-git-manager/fs/store.nu b/nu-git-manager/fs/store.nu index 890128e0..a47eca53 100644 --- a/nu-git-manager/fs/store.nu +++ b/nu-git-manager/fs/store.nu @@ -1,7 +1,7 @@ export def get-repo-store-path []: nothing -> path { $env.GIT_REPOS_HOME? | default ( $env.XDG_DATA_HOME? | default ($nu.home-path | path join ".local/share") | path join "repos" - ) + ) | path expand } export def list-repos-in-store []: nothing -> list { diff --git a/nu-git-manager/mod.nu b/nu-git-manager/mod.nu index 61fdb3e0..38a396e6 100644 --- a/nu-git-manager/mod.nu +++ b/nu-git-manager/mod.nu @@ -104,9 +104,9 @@ export def "gm list" [ # - `~/.local/share/repos` # # # Example -# a contrived example +# a contrived example, assuming you are in `~` # > GIT_REPOS_HOME=foo gm root -# foo +# ~/foo export def "gm root" []: nothing -> path { get-repo-store-path } From 5053555916efcc6841d14d1f77d3af213300b70c Mon Sep 17 00:00:00 2001 From: amtoine Date: Fri, 13 Oct 2023 18:29:11 +0200 Subject: [PATCH 11/18] add a test for getting the root --- tests/mod.nu | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/mod.nu b/tests/mod.nu index 69bbae79..96124e7e 100644 --- a/tests/mod.nu +++ b/tests/mod.nu @@ -1,6 +1,7 @@ use std assert use ../nu-git-manager/git/url.nu [parse-git-url, get-fetch-push-urls] +use ../nu-git-manager/fs/store.nu get-repo-store-path export def git-url-parsing [] { let cases = [ @@ -66,3 +67,19 @@ export def fetch-and-push-urls [] { assert equal $actual $expected $"input: ($case)" } } + +export def get-store-root [] { + let cases = [ + [env, expected]; + + [{GIT_REPOS_HOME: null, XDG_DATA_HOME: null}, "~/.local/share/repos"], + [{GIT_REPOS_HOME: "~/my_repos", XDG_DATA_HOME: null}, "~/my_repos"], + [{GIT_REPOS_HOME: null, XDG_DATA_HOME: "~/xdg"}, "~/xdg/repos"], + [{GIT_REPOS_HOME: "~/my_repos", XDG_DATA_HOME: "~/xdg"}, "~/my_repos"], + ] + + for case in $cases { + let actual = with-env $case.env { get-repo-store-path } + assert equal $actual ($case.expected | path expand) + } +} From 684f005ca21f0e3b078746087afba2ef789a30e2 Mon Sep 17 00:00:00 2001 From: melMass Date: Thu, 19 Oct 2023 18:29:23 +0200 Subject: [PATCH 12/18] fix: windows forward slashes --- nu-git-manager/git/url.nu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nu-git-manager/git/url.nu b/nu-git-manager/git/url.nu index 09d3191d..0fc8122e 100644 --- a/nu-git-manager/git/url.nu +++ b/nu-git-manager/git/url.nu @@ -26,7 +26,7 @@ export def get-fetch-push-urls [ let base_url = { scheme: null, host: $repository.host, - path: ([$repository.owner $repository.group $repository.repo] | compact | path join) + path: ([$repository.owner $repository.group $repository.repo] | compact | path join | str replace '\' '/') } let http_url = $base_url | update scheme "https" | url join let ssh_url = $base_url | update scheme "ssh" | url join From e1195fd8900e5d9662997cb51bed9631599f5a5b Mon Sep 17 00:00:00 2001 From: melMass Date: Thu, 19 Oct 2023 18:41:10 +0200 Subject: [PATCH 13/18] fix: cross platform open --- nu-git-manager/fs/dir.nu | 13 +++++++++++++ nu-git-manager/sugar/gh.nu | 7 ++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 nu-git-manager/fs/dir.nu diff --git a/nu-git-manager/fs/dir.nu b/nu-git-manager/fs/dir.nu new file mode 100644 index 00000000..2668c668 --- /dev/null +++ b/nu-git-manager/fs/dir.nu @@ -0,0 +1,13 @@ +# Cross platform wrapper to open a directory, a file or a URL in the default application +export def open-item [pth:path] { + + let cmd = match $nu.os-info.name { + "windows" => "explorer", + "macos" => "open", + "linux" => "xdg-open" + } + + ^$cmd $pth + +} + diff --git a/nu-git-manager/sugar/gh.nu b/nu-git-manager/sugar/gh.nu index 82726a42..0c800fb8 100644 --- a/nu-git-manager/sugar/gh.nu +++ b/nu-git-manager/sugar/gh.nu @@ -1,3 +1,4 @@ +use ../fs/dir.nu [open-item] def check-gh-logged-in [] { let out = (do -i { gh auth status } | complete) if $out.exit_code != 0 { @@ -75,7 +76,7 @@ export def "pr open" [ } } | url join) - xdg-open $url + open-item $url } def unpack-pages [] { @@ -163,7 +164,7 @@ export def "me pr" [ if not ($number | is-empty) { if $open_in_browser { - xdg-open ({ + open-item ({ scheme: "https" host: "github.com" path: ($repo | path join "pull" ($number | into string)) @@ -218,7 +219,7 @@ export def "me pr" [ } if $open_in_browser { - xdg-open $choice.url + open-item $choice.url return } From f7b15c30ff91e3d80886cb0f475b26fc63e1a538 Mon Sep 17 00:00:00 2001 From: melMass Date: Thu, 19 Oct 2023 18:42:02 +0200 Subject: [PATCH 14/18] fix: temp fix for windows --- nu-git-manager/fs/store.nu | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/nu-git-manager/fs/store.nu b/nu-git-manager/fs/store.nu index a47eca53..b9c9301c 100644 --- a/nu-git-manager/fs/store.nu +++ b/nu-git-manager/fs/store.nu @@ -8,9 +8,13 @@ export def list-repos-in-store []: nothing -> list { if not (get-repo-store-path | path exists) { return [] } - - # FIXME: do not use external `find` command - ^find (get-repo-store-path) -name ".git" - | lines - | each { path split | range 0..(-2) | path join } + + if $nu.os-info.name == "windows" { + # FIXME: this is super slow on windows + glob **/*.git --not [**/*.venv **/node_modules/** **/target/** **/build/** */] + } else { + # FIXME: do not use external `find` command + ^find (get-repo-store-path) -name ".git" + | lines + } | each { path split | range 0..(-2) | path join } } From 1d9fced56531694afd62943256fb5e2eb6472e13 Mon Sep 17 00:00:00 2001 From: melMass Date: Thu, 19 Oct 2023 18:48:21 +0200 Subject: [PATCH 15/18] Revert "fix: windows forward slashes" This reverts commit 684f005ca21f0e3b078746087afba2ef789a30e2. --- nu-git-manager/git/url.nu | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nu-git-manager/git/url.nu b/nu-git-manager/git/url.nu index 0fc8122e..09d3191d 100644 --- a/nu-git-manager/git/url.nu +++ b/nu-git-manager/git/url.nu @@ -26,7 +26,7 @@ export def get-fetch-push-urls [ let base_url = { scheme: null, host: $repository.host, - path: ([$repository.owner $repository.group $repository.repo] | compact | path join | str replace '\' '/') + path: ([$repository.owner $repository.group $repository.repo] | compact | path join) } let http_url = $base_url | update scheme "https" | url join let ssh_url = $base_url | update scheme "ssh" | url join From 19bade364e9508e8ad7410606c1c44ce254261e0 Mon Sep 17 00:00:00 2001 From: melMass Date: Thu, 19 Oct 2023 19:07:16 +0200 Subject: [PATCH 16/18] fix: windows forward slashes (2nd attempt) I also had to fix the tests --- nu-git-manager/fs/store.nu | 2 +- nu-git-manager/git/url.nu | 4 ++-- tests/mod.nu | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nu-git-manager/fs/store.nu b/nu-git-manager/fs/store.nu index b9c9301c..3f309579 100644 --- a/nu-git-manager/fs/store.nu +++ b/nu-git-manager/fs/store.nu @@ -1,7 +1,7 @@ export def get-repo-store-path []: nothing -> path { $env.GIT_REPOS_HOME? | default ( $env.XDG_DATA_HOME? | default ($nu.home-path | path join ".local/share") | path join "repos" - ) | path expand + ) | path expand | str replace -a '\' '/' } export def list-repos-in-store []: nothing -> list { diff --git a/nu-git-manager/git/url.nu b/nu-git-manager/git/url.nu index 09d3191d..e469448f 100644 --- a/nu-git-manager/git/url.nu +++ b/nu-git-manager/git/url.nu @@ -9,7 +9,7 @@ export def parse-git-url []: string -> record Date: Thu, 19 Oct 2023 19:54:43 +0200 Subject: [PATCH 17/18] refactor `fs/dir.nu` --- nu-git-manager/fs/dir.nu | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nu-git-manager/fs/dir.nu b/nu-git-manager/fs/dir.nu index 2668c668..dade386d 100644 --- a/nu-git-manager/fs/dir.nu +++ b/nu-git-manager/fs/dir.nu @@ -1,13 +1,10 @@ # Cross platform wrapper to open a directory, a file or a URL in the default application -export def open-item [pth:path] { - +export def open-item [file: path]: nothing -> nothing { let cmd = match $nu.os-info.name { "windows" => "explorer", "macos" => "open", "linux" => "xdg-open" } - ^$cmd $pth - + ^$cmd $file } - From 42cb06186941b258f78c86d5a9a7ee47a6ddc053 Mon Sep 17 00:00:00 2001 From: amtoine Date: Fri, 20 Oct 2023 14:52:57 +0200 Subject: [PATCH 18/18] refactor `/` manipulations in `path sanitize` and add test --- nu-git-manager/fs/path.nu | 4 ++++ nu-git-manager/fs/store.nu | 6 ++++-- nu-git-manager/git/url.nu | 11 +++++++++-- tests/mod.nu | 9 +++++++-- 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 nu-git-manager/fs/path.nu diff --git a/nu-git-manager/fs/path.nu b/nu-git-manager/fs/path.nu new file mode 100644 index 00000000..48a8007e --- /dev/null +++ b/nu-git-manager/fs/path.nu @@ -0,0 +1,4 @@ +# sanitize a Windows path +export def "path sanitize" []: path -> path { + str replace --all '\' '/' +} diff --git a/nu-git-manager/fs/store.nu b/nu-git-manager/fs/store.nu index 3f309579..3ca15f10 100644 --- a/nu-git-manager/fs/store.nu +++ b/nu-git-manager/fs/store.nu @@ -1,14 +1,16 @@ +use path.nu "path sanitize" + export def get-repo-store-path []: nothing -> path { $env.GIT_REPOS_HOME? | default ( $env.XDG_DATA_HOME? | default ($nu.home-path | path join ".local/share") | path join "repos" - ) | path expand | str replace -a '\' '/' + ) | path expand | path sanitize } export def list-repos-in-store []: nothing -> list { if not (get-repo-store-path | path exists) { return [] } - + if $nu.os-info.name == "windows" { # FIXME: this is super slow on windows glob **/*.git --not [**/*.venv **/node_modules/** **/target/** **/build/** */] diff --git a/nu-git-manager/git/url.nu b/nu-git-manager/git/url.nu index e469448f..8be004ea 100644 --- a/nu-git-manager/git/url.nu +++ b/nu-git-manager/git/url.nu @@ -1,3 +1,5 @@ +use ../fs/path.nu "path sanitize" + export def parse-git-url []: string -> record { str replace --regex '^git@(.*):' 'ssh://$1/' | str replace --regex '\.git$' '' @@ -9,7 +11,7 @@ export def parse-git-url []: string -> record