Skip to content

Commit

Permalink
clean sugar git (#104)
Browse files Browse the repository at this point in the history
related to
- #106

## description
this is an effort to
- [x] update the `nu-git-manager-sugar git` module
- [x] remove deprecated / useless commands
- [x] expose the meaningful ones as subcommands of `gm`
- [x] add tests when possible

### kept commands
- `get commit` -> `gm repo get commit`
- `root` -> `gm repo goto root`
- `branches` -> `gm repo branches`
- `is-ancestor` -> `gm repo is-ancestor`
- `remote list` -> `gm repo remote list`

### removed commands
- `operations`
- `compare`
- `lock clean`
- `remote add`
- `remote remove`
- `fixup`

### tests
a new `tests sugar git` module with
- `get-commit`
- `goto-root`
- `branches`
- `is-ancestor`
- `remote-list`
  • Loading branch information
amtoine committed Nov 22, 2023
1 parent b9a2786 commit c3ad086
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 167 deletions.
230 changes: 63 additions & 167 deletions src/nu-git-manager-sugar/git.nu
Original file line number Diff line number Diff line change
@@ -1,198 +1,94 @@
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
export def "gm repo get commit" [
revision: string = "HEAD" # the revision to get the hash of
]: nothing -> string {
# FIXME: this `str trim` sounds like a bug :thinking:
^git rev-parse $revision | str trim
}

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."
}
^git rev-parse --show-toplevel
}

# go to the root of the repository from anywhere in the worktree
export def --env root [] {
export def --env "gm repo goto root" []: nothing -> nothing {
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
# > **Note**
# > in the following, a "*dangling*" branch refers to a branch that does not have any remote
# > counterpart, i.e. it's a purely local branch.
#
# # Examples
# list branches and their associated remotes
# > gm repo branches
#
# clean all dangling branches
# > gm repo branches --clean
export def "gm repo branches" [
--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)
}
}
)
]: nothing -> table<branch: string, remotes: list<string>> {
let local_branches = ^git branch --list | lines | str replace --regex '..' ""
let remote_branches = ^git branch --remotes
| lines
| str trim
| find --invert "HEAD ->"
| parse "{remote}/{branch}"

let branches = $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 $clean {
let dangling_branches = $branches | where remotes == []

if ($dangling_branches | length) == 0 {
print "no dangling branch"
return
}
if ($dangling_branches | is-empty) {
log warning "no dangling branches"
return
}

if $clean {
$dangling_branches | each {|| git branch --delete --force $in}
for branch in $dangling_branches.branch {
log info $"deleting branch `($branch)`"
^git branch --quiet --delete --force $branch
}
} else {
$dangling_branches
$branches
}
}

# return true iif the first revision is an ancestor of the second
export def is-ancestor [
#
# # Examples
# HEAD~20 is an ancestor of HEAD
# > gm repo is-ancestor HEAD~20 HEAD
# true
#
# HEAD is never an ancestor of HEAD~20
# > gm repo is-ancestor HEAD HEAD~20
# false
export def "gm repo 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
]: nothing -> bool {
(do -i { ^git merge-base $a $b --is-ancestor } | complete | get exit_code) == 0
}

# get the list of all the remotes in the current repository
export def "remote list" [] {
export def "gm repo remote list" []: nothing -> table<remote: string, fetch: string, push: string> {
# FIXME: use the helper `list-remotes` command from ../nu-git-manager/git/repo.nu:29
^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) {
error make {
msg: $"(ansi red_bold)remote_already_in_index(ansi reset)"
label: {
text: $"already a remote of ($env.PWD)"
span: (metadata $name | get span)
}
| detect columns --no-headers
| rename remote url mode
| group-by remote
| transpose
| update column1 {
reject remote | select mode url | transpose --header-row | into record
}
}

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 {
error make {
msg: $"(ansi red_bold)revision_not_found(ansi reset)"
label: {
text: $"($revision) not found in the working tree of ($env.PWD)"
span: (metadata $revision | get span)
}
}
}

git commit --fixup $revision
git rebase --interactive --autosquash $"($revision)~1"
| flatten
| rename remote fetch push
}
2 changes: 2 additions & 0 deletions src/nu-git-manager/git/repo.nu
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export def get-root-commit [
]: nothing -> string {
let repo = $repo | default (pwd)

# FIXME: this is a bug and should work without string interpolation
# see https://github.com/nushell/nushell/issues/11134
$"(^git -C $repo rev-list HEAD | lines | last)"
}

Expand Down
65 changes: 65 additions & 0 deletions tests/sugar/git.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std assert

use ../../src/nu-git-manager-sugar/ git *
use ../../src/nu-git-manager/fs/path.nu ["path sanitize"]
use ../common/setup.nu [get-random-test-dir]

def --env init-repo-and-cd-into []: nothing -> path {
let repo = get-random-test-dir

^git init $repo
cd $repo

$repo
}

export def get-commit [] {
init-repo-and-cd-into

^git checkout --orphan main
^git commit --allow-empty --no-gpg-sign --message "init"

assert equal (gm repo get commit) (^git rev-parse HEAD)
}

export def goto-root [] {
let repo = init-repo-and-cd-into

mkdir init-repo-and-cd-into/bar/baz
cd init-repo-and-cd-into/bar/baz

gm repo goto root
assert equal (pwd | path sanitize) $repo
}

export def branches [] {
init-repo-and-cd-into

assert equal (gm repo branches) []

^git checkout --orphan foo
^git commit --allow-empty --no-gpg-sign --message "init"

assert equal (gm repo branches) [{branch: foo, remotes: []}]
}

export def is-ancestor [] {
init-repo-and-cd-into

^git commit --allow-empty --no-gpg-sign --message "init"
^git commit --allow-empty --no-gpg-sign --message "c1"
^git commit --allow-empty --no-gpg-sign --message "c2"

assert (gm repo is-ancestor HEAD^ HEAD)
assert not (gm repo is-ancestor HEAD HEAD^)
}

export def remote-list [] {
init-repo-and-cd-into

assert equal (gm repo remote list) []

^git remote add foo foo-url

assert equal (gm repo remote list) [{remote: foo, fetch: foo-url, push: foo-url}]
}
Empty file added tests/sugar/mod.nu
Empty file.

0 comments on commit c3ad086

Please sign in to comment.