Merge pull request #412 from RShirohara/add-partial-clone-support

feat: support partial clone on Git repository
This commit is contained in:
Masayuki Matsuki 2025-03-26 00:34:04 +09:00 committed by GitHub
commit e0e71a3449
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 106 additions and 5 deletions

View file

@ -16,7 +16,7 @@ You can also list local repositories (+ghq list+).
== SYNOPSIS
[verse]
ghq get [-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch] [--no-recursive] [--bare] <repository URL>|<host>/<user>/<project>|<user>/<project>|<project>
ghq get [-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch] [--no-recursive] [--bare] [--partial blobless|treeless] <repository URL>|<host>/<user>/<project>|<user>/<project>|<project>
ghq list [-p] [-e] [<query>]
ghq create [--vcs <vcs>] <repository URL>|<host>/<user>/<project>|<user>/<project>|<project>
ghq rm [--dry-run] <repository URL>|<host>/<user>/<project>|<user>/<project>|<project>
@ -44,7 +44,10 @@ get::
The 'ghq' gets the git repository recursively by default. +
We can prevent it with '--no-recursive' option.
With '--bare' option, a "bare clone" will be performed (for Git
repositories only, 'git clone --bare ...' eg.).
repositories only, 'git clone --bare ...' eg.). +
With '--partial' option, a "partial clone" will be performed (for Git
repositories only, in 'blobless' mode, 'git clone --filter=blob:none ...',
in 'treeless' mode, 'git clone --filter=tree:0 ...' eg.).
list::
List locally cloned repositories. If a query argument is given, only

View file

@ -35,6 +35,7 @@ func doGet(c *cli.Context) error {
branch: c.String("branch"),
recursive: !c.Bool("no-recursive"),
bare: c.Bool("bare"),
partial: c.String("partial"),
}
if parallel {
// force silent in parallel import

View file

@ -248,6 +248,52 @@ func TestCommandGet(t *testing.T) {
t.Errorf("silent mode should not output any logs, but got: %s", out)
}
},
}, {
name: "[partial] blobless",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
app.Run([]string{"", "get", "--partial", "blobless", "motemen/ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
t.Errorf("got: %s, expect: %s", cloneArgs.remote, expect)
}
if filepath.ToSlash(cloneArgs.local) != filepath.ToSlash(localDir) {
t.Errorf("got: %s, expect: %s", filepath.ToSlash(cloneArgs.local), filepath.ToSlash(localDir))
}
if cloneArgs.partial != "blobless" {
t.Errorf("cloneArgs.partial should be \"blobless\"")
}
},
}, {
name: "[partial] treeless",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
app.Run([]string{"", "get", "--partial", "treeless", "motemen/ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
t.Errorf("got: %s, expect: %s", cloneArgs.remote, expect)
}
if filepath.ToSlash(cloneArgs.local) != filepath.ToSlash(localDir) {
t.Errorf("got: %s, expect: %s", filepath.ToSlash(cloneArgs.local), filepath.ToSlash(localDir))
}
if cloneArgs.partial != "treeless" {
t.Errorf("cloneArgs.partial should be \"treeless\"")
}
},
}, {
name: "[partial] unacceptable value",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
err := app.Run([]string{"", "get", "--partial", "unacceptable", "motemen/ghq-test-repo"})
expect := "flag partial value \"unacceptable\" is not allowed"
if err.Error() != expect {
t.Errorf("got: %s, expect: %s", err.Error(), expect)
}
},
}}
for _, tc := range testCases {

View file

@ -2,6 +2,7 @@ package main
import (
"fmt"
"slices"
"strings"
"github.com/urfave/cli/v2"
@ -38,6 +39,16 @@ var commandGet = &cli.Command{
Usage: "Specify `branch` name. This flag implies --single-branch on Git"},
&cli.BoolFlag{Name: "parallel", Aliases: []string{"P"}, Usage: "Import parallelly"},
&cli.BoolFlag{Name: "bare", Usage: "Do a bare clone"},
&cli.StringFlag{
Name: "partial",
Usage: "Do a partial clone. Can specify either \"blobless\" or \"treeless\"",
Action: func(ctx *cli.Context, v string) error {
expected := []string{"blobless", "treeless"}
if !slices.Contains(expected, v) {
return fmt.Errorf("flag partial value \"%v\" is not allowed", v)
}
return nil
}},
},
}
@ -94,7 +105,7 @@ type commandDoc struct {
}
var commandDocs = map[string]commandDoc{
"get": {"", "[-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch <branch>] [--no-recursive] [--bare] <repository URL>|<project>|<user>/<project>|<host>/<user>/<project>"},
"get": {"", "[-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch <branch>] [--no-recursive] [--bare] [--partial blobless|treeless] <repository URL>|<project>|<user>/<project>|<host>/<user>/<project>"},
"list": {"", "[-p] [-e] [<query>]"},
"create": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},
"rm": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},

View file

@ -15,6 +15,7 @@ type _cloneArgs struct {
recursive bool
bare bool
silent bool
partial string
}
type _updateArgs struct {
@ -41,6 +42,7 @@ func withFakeGitBackend(t *testing.T, block func(*testing.T, string, *_cloneArgs
recursive: vg.recursive,
bare: vg.bare,
silent: vg.silent,
partial: vg.partial,
}
return nil
},

View file

@ -25,7 +25,7 @@ type getInfo struct {
type getter struct {
update, shallow, silent, ssh, recursive, bare bool
vcs, branch string
vcs, branch, partial string
}
func (g *getter) get(argURL string) (getInfo, error) {
@ -113,6 +113,7 @@ func (g *getter) getRemoteRepository(remote RemoteRepository, branch string) (ge
branch: branch,
recursive: g.recursive,
bare: g.bare,
partial: g.partial,
})
}
return info, nil

View file

@ -24,6 +24,8 @@ export GHQ_ROOT=$tmpdir
ghq get https://svn.apache.org/repos/asf/subversion
ghq get --shallow hub.darcs.net/byorgey/split
ghq get --bare x-motemen/gore
ghq get --partial blobless x-motemen/blogsync
ghq get --partial treeless x-motemen/gobump
test -d $tmpdir/github.com/x-motemen/ghq/.git
test -d $tmpdir/www.mercurial-scm.org/repo/hello/.hg
@ -34,11 +36,15 @@ export GHQ_ROOT=$tmpdir
test -d $tmpdir/svn.apache.org/repos/asf/subversion/.svn
test -d $tmpdir/hub.darcs.net/byorgey/split/_darcs
test -d $tmpdir/github.com/x-motemen/gore.git/refs
grep --quiet "partialclonefilter = blob:none" $tmpdir/github.com/x-motemen/blogsync/.git/config
grep --quiet "partialclonefilter = tree:0" $tmpdir/github.com/x-motemen/gobump/.git/config
: testing 'ghq list'
cat <<EOF | sort > $tmpdir/expect
chiselapp.com/user/sti/repository/fossil-gui
github.com/x-motemen/blogsync
github.com/x-motemen/ghq
github.com/x-motemen/gobump
github.com/x-motemen/gore.git
www.mercurial-scm.org/repo/hello
launchpad.net/shutter

View file

@ -36,6 +36,11 @@ complete -c ghq -n '__fish_seen_subcommand_from get' -l no-recursive -d 'Prevent
complete -c ghq -n '__fish_seen_subcommand_from get' -s b -l branch -d 'Specify branch name. This flag implies --single-branch on Git'
complete -c ghq -n '__fish_seen_subcommand_from get' -s P -l parallel -d 'Import parallelly'
complete -c ghq -n '__fish_seen_subcommand_from get' -l bare -d 'Do a bare clone'
function __complete_get_partial
printf '%s\t%s\n' 'blobless' 'Do a blobless clone'
printf '%s\t%s\n' 'treeless' 'Do a treeless clone'
end
complete -c ghq -n '__fish_seen_subcommand_from get' -l partial -d 'Do a partial clone' -xa '(__complete_get_partial)'
complete -c ghq -n '__fish_seen_subcommand_from list' -s e -l exact -d 'Perform an exact match'
complete -c ghq -n '__fish_seen_subcommand_from list' -l vcs -d 'Specify vcs backend for matching'

View file

@ -27,6 +27,7 @@ function _ghq () {
'--bare[Do a bare clone]' \
'(-b --branch)'{-b,--branch}'[Specify branch name]' \
'(-P --parallel)'{-P,--parallel}'[Import parallelly]' \
'--partial[Do a partial clone]: :(blobless treeless)' \
'(-)*:: :->null_state' \
&& ret=0
;;

7
vcs.go
View file

@ -44,7 +44,7 @@ type vcsGetOption struct {
url *url.URL
dir string
recursive, shallow, silent, bare bool
branch string
branch, partial string
}
// GitBackend is the VCSBackend of git
@ -70,6 +70,11 @@ var GitBackend = &VCSBackend{
if vg.bare {
args = append(args, "--bare")
}
if vg.partial == "blobless" {
args = append(args, "--filter=blob:none")
} else if vg.partial == "treeless" {
args = append(args, "--filter=tree:0")
}
args = append(args, vg.url.String(), vg.dir)
return run(vg.silent)("git", args...)

View file

@ -136,6 +136,26 @@ func TestVCSBackend(t *testing.T) {
})
},
expect: []string{"git", "clone", "--bare", remoteDummyURL.String(), localDir},
}, {
name: "[git] (partial) blobless clone",
f: func() error {
return GitBackend.Clone(&vcsGetOption{
url: remoteDummyURL,
dir: localDir,
partial: "blobless",
})
},
expect: []string{"git", "clone", "--filter=blob:none", remoteDummyURL.String(), localDir},
}, {
name: "[git] (partial) treeless clone",
f: func() error {
return GitBackend.Clone(&vcsGetOption{
url: remoteDummyURL,
dir: localDir,
partial: "treeless",
})
},
expect: []string{"git", "clone", "--filter=tree:0", remoteDummyURL.String(), localDir},
}, {
name: "[git] switch git-svn on update",
f: func() error {