Skip to content

Commit

Permalink
allow to merge forks (#63)
Browse files Browse the repository at this point in the history
Closes #52

---------

Co-authored-by: Mel Massadian <[email protected]>
  • Loading branch information
amtoine and melMass authored Nov 18, 2023
1 parent cedd3d2 commit 137dbfa
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 25 deletions.
8 changes: 4 additions & 4 deletions .github/actions/sync_action/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ runs:
- name: Set up Git
shell: bash
run: |
git config user.name "$(git log -n 1 --pretty=format:%an)"
git config user.email "$(git log -n 1 --pretty=format:%ae)"
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Show Git config and worktree
shell: bash
run: |
git config --list | grep user.name
git config --list | grep user.email
git config user.name
git config user.email
git log --graph --branches --remotes --tags --oneline --decorate
if: ${{ inputs.log_worktree == 'true' }}

Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/nupm-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ jobs:
with:
nu_version: ${{github.ref == 'refs/heads/nightly' && 'nightly' || inputs.nu_version}}

- name: Set up Git
shell: bash
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global init.defaultBranch main
- name: Run the tests
run: |
use ${{ steps.nu-setup.outputs.nupm_path }}
Expand Down
17 changes: 17 additions & 0 deletions src/nu-git-manager/error/error.nu
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,20 @@ export def throw-error [
}
}
}

# throw a nice warning with a standard format
export def throw-warning [
warning: record<msg: string, label: record<text: string, span: record<start: int, end: int>>>
] {
# FIXME: would be cool to have a proper way to do that :thinking:
# ^$nu.current-exe --commands $"error make {
# msg: $'\(ansi yellow_bold\)($warning.msg)\(ansi reset\)'
# label: {
# text: '($warning.label.text)'
# start: ($warning.label.span.start)
# end: ($warning.label.span.end)
# }
# }"
print $"Warning: (char -u 26a0) (ansi yellow_bold)($warning.msg)(ansi reset)"
print ($warning.label.text | lines | each { $"| ($in)" } | str join "\n")
}
16 changes: 10 additions & 6 deletions src/nu-git-manager/fs/cache.nu
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use path.nu "path sanitize"
use ../git/repo.nu [is-grafted]

# get the path to the cache of the local store of repos
#
Expand Down Expand Up @@ -29,24 +30,27 @@ export def check-cache-file [cache_file: path]: nothing -> nothing {
# open the cache file
#
# /!\ this command will return sanitized paths if `add-to-cache` or `gm update-cache` have been used. /!\
export def open-cache [cache_file: path]: nothing -> list<path> {
export def open-cache [cache_file: path]: nothing -> table<path: path, grafted: bool, root_hash: string> {
open --raw $cache_file | from nuon
}

# save a list of paths to the cache file
#
# /!\ this command will sanitize the paths for the caller. /!\
export def save-cache [cache_file: path]: list<path> -> nothing {
each { path sanitize } | to nuon | save --force $cache_file
export def save-cache [cache_file: path]: table<path: path, grafted: bool, root_hash: string> -> nothing {
update path { path sanitize } | to nuon | save --force $cache_file
}

# add a new path to the cache file
#
# /!\ this command will sanitize the paths for the caller. /!\
export def add-to-cache [cache_file: path, new_path: path]: nothing -> nothing {
export def add-to-cache [
cache_file: path,
repo: record<path: string, grafted: bool, root_hash: string>
]: nothing -> nothing {
print --no-newline "updating cache... "
open-cache $cache_file
| append ($new_path | path sanitize)
| append $repo
| uniq
| sort
| save-cache $cache_file
Expand All @@ -58,7 +62,7 @@ export def add-to-cache [cache_file: path, new_path: path]: nothing -> nothing {
# /!\ this command will sanitize the paths for the caller. /!\
export def remove-from-cache [cache_file: path, old_path: path]: nothing -> nothing {
print --no-newline "updating cache... "
open-cache $cache_file | where $it != ($old_path | path sanitize) | save-cache $cache_file
open-cache $cache_file | where $it.path != ($old_path | path sanitize) | save-cache $cache_file
print "done"
}

Expand Down
40 changes: 40 additions & 0 deletions src/nu-git-manager/git/repo.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use ../error/error.nu [throw-error]

# tell if a local repository has been grafted
#
# > see more about grafted commits
# > [here](https://stackoverflow.com/questions/27296188/what-exactly-is-a-grafted-commit-in-a-shallow-clone)
export def is-grafted [
repo?: path, # the path to the repository to check (defaults to `pwd`)
]: nothing -> bool {
let repo = $repo | default (pwd)

not (
^git -C $repo log --oneline --decorate (get-root-commit $repo)
| parse --regex '[0-9a-f]+ \(grafted.*'
| is-empty
)
}

# compute the hash of the root commit of a repository
export def get-root-commit [
repo?: path, # the path to the repository to check (defaults to `pwd`)
]: nothing -> string {
let repo = $repo | default (pwd)

$"(^git -C $repo rev-list HEAD | lines | last)"
}

# wrapper around `git remote --verbose show` to list the remotes of a repository
export def list-remotes [
repo?: path, # the path to the repository to check (defaults to `pwd`)
]: nothing -> table<remote: string, fetch: string, push: string> {
^git -C ($repo | default (pwd)) remote --verbose show
| detect columns --no-headers
| rename remote url mode
| group-by remote
| transpose k v
| update v { reject remote | select mode url | transpose --header-row | into record }
| flatten
| rename remote fetch push
}
151 changes: 145 additions & 6 deletions src/nu-git-manager/mod.nu
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use fs/cache.nu [
use fs/dir.nu [clean-empty-directories-rec]
use fs/path.nu ["path sanitize"]
use git/url.nu [parse-git-url, get-fetch-push-urls]
use error/error.nu [throw-error]
use git/repo.nu [is-grafted, get-root-commit, list-remotes]
use error/error.nu [throw-error, throw-warning]

use completions/nu-complete.nu

Expand Down Expand Up @@ -118,9 +119,54 @@ export def "gm clone" [
^git -C $local_path remote set-url $remote $urls.fetch
^git -C $local_path remote set-url $remote --push $urls.push

let repo = {
path: $local_path,
grafted: (is-grafted $local_path),
root_hash: (get-root-commit $local_path)
}

let cache_file = get-repo-store-cache-path
check-cache-file $cache_file
add-to-cache $cache_file $local_path

if $repo.grafted {
throw-warning {
msg: "cloning_grafted_repository",
label: {
text: "this repo is grafted, cannot detect forks",
span: (metadata $url).span
}
}
} else {
let forks = open-cache $cache_file | where root_hash == $repo.root_hash

if not ($forks | is-empty) {
let msg = if ($forks | length) == 1 {
"1 other repo"
} else {
$"($forks | length) other repos"
}
throw-warning {
msg: "cloning_fork"
label: {
text: (
$"this repo is a fork of (ansi cyan)($msg)(ansi reset) because they share the same root commit: (ansi magenta)($repo.root_hash)(ansi reset)\n"
+ (
$forks | get path | each {
let repo = $in
| str replace (get-repo-store-path) ''
| str trim --left --char "/"
$"- (ansi cyan)($repo)(ansi reset)"
} | str join "\n"
)

)
span: (metadata $url).span
}
}
}
}

add-to-cache $cache_file $repo

null
}
Expand All @@ -145,9 +191,9 @@ export def "gm list" [
check-cache-file $cache_file

if $full_path {
open-cache $cache_file
open-cache $cache_file | get path
} else {
open-cache $cache_file | each {
open-cache $cache_file | get path | each {
str replace (get-repo-store-path) '' | str trim --left --char "/"
}
}
Expand Down Expand Up @@ -197,7 +243,7 @@ export def "gm status" []: nothing -> record<root: record<path: path, exists: bo
let cache_exists = ($cache | path type) == "file"

let missing = if $cache_exists {
open-cache $cache | where ($it | path type) != "dir"
open-cache $cache | get path | where ($it | path type) != "dir"
} else {
null
}
Expand Down Expand Up @@ -226,7 +272,11 @@ export def "gm update-cache" []: nothing -> nothing {
clean-cache-dir $cache_file

print --no-newline "updating cache... "
list-repos-in-store | save-cache $cache_file
list-repos-in-store | each {{
path: $in,
grafted: (is-grafted $in),
root_hash: (get-root-commit $in)
}} | save-cache $cache_file
print "done"

null
Expand Down Expand Up @@ -340,6 +390,95 @@ export def "gm remove" [
null
}

# squash multi-directory forks into a single repo
#
# Here, two forks are defined as *two non-grafted repositories that share the same initial commit,
# i.e. that have the same root hash in their respective DAGs*.
#
# By default, `gm squash-forks` will prompt the user for a main fork for each repository with
# multiple forks.
# Once a *main* fork has been chosen, for each one of the other secondary forks, the command will
# preform the following steps:
# - add the secondary fork as a remote to the main one
# - setup the FETCH and PUSH remotes to the same ones as the secondary fork in the main one
# - remove the secondary fork entirely from the store and the cache
#
# This operation can be done in a non-interactive manner by specifying `--non-interactive-preselect`.
# This option is a `record` with
# - keys: the root hash of repos, e.g. [2ed2d87](https://github.com/amtoine/nu-git-manager/commit/2ed2d875d80505d78423328c6b2a60522715fcdf) for `nu-git-manager`
# - values: the main fork to select in full-name form, e.g. `github.com/amtoine/nu-git-manager`
#
# # Examples
# squash forks interactively
# > gm squash-forks
#
# squash forks non-interactively: `nu-git-manager` and `nushell` to the forks of @amtoine
# > gm squash-forks --non-interactive-preselect {
# 2ed2d875d80505d78423328c6b2a60522715fcdf: "github.com/amtoine/nu-git-manager",
# 8f3b273337b53bd86d5594d5edc9d4ad7242bd4c: "github.com/amtoine/nushell",
# }
export def "gm squash-forks" [
--non-interactive-preselect: record = {} # the non-interactive preselection record, see documentation above
]: nothing -> nothing {
let status = gm status

let forks_to_squash = open $status.cache.path --raw
| from nuon
| group-by root_hash
| transpose k v
| where ($it.v | length) > 1
| get v

if ($forks_to_squash | is-empty) {
log info "no forks to squash"
return
}

$forks_to_squash | each {|forks|
let default = $non_interactive_preselect | get --ignore-errors $forks.root_hash.0
let main = if $default == null {
let choice = $forks.path
| str replace $status.root.path ''
| str trim --char '/'
| input list $"Please choose a main fork to squash ($forks.root_hash.0)"
if ($choice | is-empty) {
log warning $"skipping ($forks.root_hash.0)"
continue
}
$choice
} else {
$default
}

log debug $"squashing into ($main)"

let main = $status.root.path | path join $main | path sanitize
for fork in $forks.path {
if $fork != $main {
let fork_origin = list-remotes $fork | where remote == "origin" | into record

let fork_name = $fork | path split | reverse | get 1
let fork_full_name = $fork | str replace $status.root.path '' | str trim --char '/'

log debug $" squashing ($fork_full_name)"

log debug $" adding remote ($fork_name)"
^git -C $main remote add ($fork_name) "PLACEHOLDER"

log debug $" setting FETCH to ($fork_origin.fetch)"
^git -C $main remote set-url ($fork_name) $fork_origin.fetch
log debug $" setting PUSH to ($fork_origin.push)"
^git -C $main remote set-url --push ($fork_name) $fork_origin.push

log debug $" removing ($fork_full_name)"
gm remove --no-confirm $fork_full_name
}
}
}

null
}

# clean the store
#
# this command will mainly remove empty directory recursively.
Expand Down
Loading

0 comments on commit 137dbfa

Please sign in to comment.