feat: remote template repositories (#437)

* feat: remote template repositories

* Update cmd/create.go

Co-authored-by: Lance Ball <lball@redhat.com>

* docs: extensible templates

* feat: remote template repositories

* Update docs/guides/language-packs.md

* Update docs/guides/language-packs.md

Co-authored-by: Lance Ball <lball@redhat.com>

* Update docs/guides/language-packs.md

Co-authored-by: Lance Ball <lball@redhat.com>

* Update docs/guides/templates.md

Co-authored-by: Lance Ball <lball@redhat.com>

Co-authored-by: Lance Ball <lball@redhat.com>
This commit is contained in:
Luke Kingland 2021-08-03 22:28:15 +09:00 committed by GitHub
parent 0dba67751e
commit 9db1a3d902
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 401 additions and 107 deletions

View File

@ -34,6 +34,7 @@ type Client struct {
describer Describer
dnsProvider DNSProvider // Provider of DNS services
repositories string // path to extensible template repositories
repository string // URL to Git repo (overrides on-disk and embedded)
registry string // default registry for OCI image tags
progressListener ProgressListener // progress listener
emitter Emitter // Emits CloudEvents to functions
@ -259,6 +260,15 @@ func WithRepositories(repositories string) Option {
}
}
// WithRepository sets a specific URL to a Git repository from which to pull templates.
// This setting's existence precldes the use of either the inbuilt templates or any
// repositories from the extensible repositories path.
func WithRepository(repository string) Option {
return func(c *Client) {
c.repository = repository
}
}
// WithRegistry sets the default registry which is consulted when an image name/tag
// is not explocitly provided. Can be fully qualified, including the registry
// (ex: 'quay.io/myname') or simply the namespace 'myname' which indicates the
@ -342,7 +352,8 @@ func (c *Client) Create(cfg Function) (err error) {
return
}
// Create Function of the given root path.
// Create Function about the given root path.
// Loads extant config if it exists. In-memory representation only.
f, err := NewFunction(cfg.Root)
if err != nil {
return
@ -371,7 +382,7 @@ func (c *Client) Create(cfg Function) (err error) {
}
// Write out a template.
w := templateWriter{templates: c.repositories, verbose: c.verbose}
w := templateWriter{repositories: c.repositories, url: c.repository, verbose: c.verbose}
if err = w.Write(f.Runtime, f.Template, f.Root); err != nil {
return
}

View File

@ -2,6 +2,7 @@ package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/AlecAivazis/survey/v2"
@ -27,6 +28,7 @@ func init() {
func newCreateClient(cfg createConfig) *fn.Client {
return fn.New(
fn.WithRepositories(cfg.Repositories),
fn.WithRepository(cfg.Repository),
fn.WithVerbose(cfg.Verbose))
}
@ -58,13 +60,13 @@ kn func create --runtime quarkus myfunc
kn func create --template events myfunc
`,
SuggestFor: []string{"vreate", "creaet", "craete", "new"},
PreRunE: bindEnv("runtime", "template", "repositories", "confirm"),
PreRunE: bindEnv("runtime", "template", "repository", "confirm"),
}
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
cmd.Flags().StringP("runtime", "l", fn.DefaultRuntime, "Function runtime language/framework. Available runtimes: "+buildpacks.Runtimes()+" (Env: $FUNC_RUNTIME)")
cmd.Flags().StringP("repositories", "r", filepath.Join(configPath(), "repositories"), "Path to extended template repositories (Env: $FUNC_REPOSITORIES)")
cmd.Flags().StringP("template", "t", fn.DefaultTemplate, "Function template. Available templates: 'http' and 'events' (Env: $FUNC_TEMPLATE)")
cmd.Flags().StringP("repository", "r", "", "URI to a Git repository containing the specified template (Env: $FUNC_REPOSITORY)")
// Register tab-completeion function integration
if err := cmd.RegisterFlagCompletionFunc("runtime", CompleteRuntimeList); err != nil {
@ -117,11 +119,16 @@ type createConfig struct {
Runtime string
// Repositories is an optional path that, if it exists, will be used as a source
// for additional template repositories not included in the binary. If not provided
// explicitly as a flag (--repositories) or env (FUNC_REPOSITORIES), the default
// location is $XDG_CONFIG_HOME/repositories ($HOME/.config/func/repositories)
// for additional template repositories not included in the binary. provided via
// env (FUNC_REPOSITORIES), the default location is $XDG_CONFIG_HOME/repositories
// ($HOME/.config/func/repositories)
Repositories string
// Repository is the URL of a specific Git repository to use for templates.
// If specified, this takes precidence over both inbuilt templates or
// extensible templates.
Repository string
// Template is the code written into the new Function project, including
// an implementation adhering to one of the supported function signatures.
// May also include additional configuration settings or examples.
@ -148,15 +155,24 @@ func newCreateConfig(args []string) createConfig {
}
derivedName, derivedPath := deriveNameAndAbsolutePathFromPath(path)
return createConfig{
Name: derivedName,
Path: derivedPath,
Repositories: viper.GetString("repositories"),
Runtime: viper.GetString("runtime"),
Template: viper.GetString("template"),
Confirm: viper.GetBool("confirm"),
Verbose: viper.GetBool("verbose"),
cc := createConfig{
Name: derivedName,
Path: derivedPath,
Repository: viper.GetString("repository"),
Runtime: viper.GetString("runtime"),
Template: viper.GetString("template"),
Confirm: viper.GetBool("confirm"),
Verbose: viper.GetBool("verbose"),
}
// Repositories not exposed as a flag due to potential confusion and
// unlikliness of being needed, but is left available as an env.
cc.Repositories = os.Getenv("FUNC_REPOSITORIES")
if cc.Repositories == "" {
cc.Repositories = filepath.Join(configPath(), "repositories")
}
return cc
}
// Prompt the user with value of config members, allowing for interaractive changes.
@ -165,10 +181,13 @@ func newCreateConfig(args []string) createConfig {
func (c createConfig) Prompt() (createConfig, error) {
if !interactiveTerminal() || !c.Confirm {
// Just print the basics if not confirming
fmt.Printf("Project path: %v\n", c.Path)
fmt.Printf("Project path: %v\n", c.Path)
fmt.Printf("Function name: %v\n", c.Name)
fmt.Printf("Runtime: %v\n", c.Runtime)
fmt.Printf("Template: %v\n", c.Template)
fmt.Printf("Runtime: %v\n", c.Runtime)
fmt.Printf("Template: %v\n", c.Template)
if c.Repository != "" {
fmt.Printf("Repository: %v\n", c.Repository)
}
return c, nil
}

View File

@ -6,16 +6,18 @@ Creates a new Function project at _`path`_. If _`path`_ is unspecified, assumes
Function name must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?').
The files written upon create include an example Function of the specified language runtime, example tests, and a metadata file `func.yaml`. Together, these are referred to as a Template. Included are the templates 'http' and 'events' (default is 'http') for each language runtime. A template can be pulled from a specific Git repository by providing the `--repository` flag, or from a locally installed repository using the repository's name as a prefix. See the [Templates Guide](templates.md) for more information.
Similar `kn` command: none.
```console
func create <path> [-l <runtime> -t <template>]
func create <path> [-l <runtime> -t <template> -r <repository>]
```
When run as a `kn` plugin.
```console
kn func create <path> [-l <runtime> -t <template>]
kn func create <path> [-l <runtime> -t <template> -r <repository>]
```
## `build`

View File

@ -0,0 +1,10 @@
# Language Packs
A Language Pack is the mechanism by which the Functions binary can be extended to support additional runtimes, function signatures, even operating systems and installed tooling for a function. A Language Pack includes
- a .builders.yaml file containing a reference to a builder OCI image reference, which conforms to the buildpack builder specification, and contains references to buildpacks supporting this Language Pack
- one or more template directories illustrating the Language Pack's recognized function signatures
- tests and documentation for the templates
Built in to the Functions library is a basic language pack for each supported language.
For an example external language pack, see [https://github.com/lance/gcf-kn/tree/main/ruby]

40
docs/guides/templates.md Normal file
View File

@ -0,0 +1,40 @@
# Templates
When a Function is created, an example implementation and a Function metadata file are written into the new Function's working directory. Together, these files are referred to as the Function's Template. Included are the templates 'http' and 'events' for each supported language runtime.
These embedded templates are minimal by design. The Function contains a minimum of external dependencies, and the 'func.yaml' defines a final environment within which the Funciton will execute that is devoid of any extraneous packages or services.
To make use of more complex inital Function implementions, or to define runtime environments with arbitrarily complex requirements, the templates system is fully pluggable.
## External Git Repositories
When creating a new Function, a Git repository can be specified as the source for the template files. For example, the Boson Project maintains a set of example Functions at https://github.com/boson-project/templates which can be used during project creation.
For example, the Boson Project Templates repository contains an example "Hello World" Function implementation in each of the officially supported languages. To use this template via the CLI, use the flags:
func create <name> --template hello-world --repository https://github.com/boson-project/templates
## Locally Installing Repositories
Template Repositories can also be installed locally by placing them in the Functions configuration directory.
To install the Boson Project templates locally, for example, clone the repository and name it `boson` using `git clone https://github.com/boson-project/templats ~/.config/func/repositories/boson`
Once installed, the Boson Hello World template can be specified:
func create <name> --template boson/hello-world
## Language Packs
In addition to example implementations, a template includes a `func.yaml` which includes metadata about the Function. By default this is populated with things like the new Function's name. It also includes a reference to the specific tooling which compiles and packages the Function into its deployable form. This is called the Builder. By customizing this metadata, it is more than just a template; it is referred to as a Language Pack. See [Project Configuration with func.yaml](func_yaml.md).
A Language Pack can support additional function signatures and can fully customize the environment of the final running Function. For more information see the [Language Pack Guide](language-packs.md).

2
go.mod
View File

@ -10,6 +10,8 @@ require (
github.com/docker/docker v20.10.7+incompatible
github.com/docker/docker-credential-helpers v0.6.4
github.com/docker/go-connections v0.4.0
github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.2
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.2.0
github.com/markbates/pkger v0.17.1

33
go.sum
View File

@ -86,6 +86,8 @@ github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tT
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331 h1:3YnB7Hpmh1lPecPE8doMOtYCrMdrpedZOvxfuNES/Vk=
github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3HV0OhsddkmM=
github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns=
github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
@ -94,6 +96,8 @@ github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nB
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
@ -106,6 +110,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/ahmetb/gen-crd-api-reference-docs v0.3.1-0.20210420163308-c1402a70e2f1/go.mod h1:TdjdkYhlOifCQWPs1UdTma97kQQMozf5h26hTuG70u8=
@ -314,6 +320,15 @@ github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -547,6 +562,8 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
@ -557,6 +574,7 @@ github.com/influxdata/tdigest v0.0.1/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQT
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -586,6 +604,8 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -599,6 +619,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
@ -627,6 +649,8 @@ github.com/mailru/easyjson v0.7.1-0.20191009090205-6c0755d89d1e/go.mod h1:KAzv3t
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno=
github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -1034,8 +1058,11 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc h1:+q90ECDSAQirdykUN6sPEiBXBsp8Csjcca8Oy7bgLTA=
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1124,6 +1151,7 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
@ -1224,9 +1252,12 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
@ -1449,6 +1480,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/evanphx/json-patch.v4 v4.9.0 h1:T7W7A7+DTEpLTC11pkf8yfaeRfqhRj/gOPf+LtaJdNY=

View File

@ -13,26 +13,30 @@ import (
"sort"
"strings"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/markbates/pkger"
)
// fileAccessor encapsulates methods for accessing template files.
type fileAccessor interface {
type filesystem interface {
Stat(name string) (os.FileInfo, error)
Open(p string) (file, error)
Open(path string) (file, error)
ReadDir(path string) ([]os.FileInfo, error)
}
type file interface {
Readdir(int) ([]os.FileInfo, error)
Read([]byte) (int, error)
Close() error
io.Reader
io.Closer
}
// When pkger is run, code analysis detects this Include statement,
// triggering the serialization of the templates directory and all
// its contents into pkged.go, which is then made available via
// a pkger fileAccessor.
// Path is relative to the go module root.
// Trigger encoding of ./templates as pkged.go
//
// When pkger is run, code analysis detects this pkger.Include statement,
// triggering the serialization of the templates directory and all its contents
// into pkged.go, which is then made available via a pkger filesystem. Path is
// relative to the go module root.
func init() {
_ = pkger.Include("/templates")
}
@ -51,8 +55,15 @@ type templateWriter struct {
// w.Write("go", "boson/http")
// Ie. "Using the custom templates in the func configuration directory,
// write the Boson HTTP template for the Go runtime."
templates string
verbose bool
repositories string
// URL of a a specific network-available Git repository to use for
// templates. Takes precidence over both builtin and extensible
// if defined.
url string
// enable verbose logging
verbose bool
}
var (
@ -64,100 +75,113 @@ var (
)
func (t templateWriter) Write(runtime, template, dest string) error {
if runtime == "" {
runtime = DefaultRuntime
}
if template == "" {
template = DefaultTemplate
}
if isCustom(template) {
return writeCustom(t.templates, runtime, template, dest)
// remote URLs, when provided, take precidence
if t.url != "" {
return writeRemote(t.url, runtime, template, dest)
}
// templates with repo prefix are on-disk "custom" (not embedded)
if len(strings.Split(template, "/")) > 1 {
return writeCustom(t.repositories, runtime, template, dest)
}
// default case is to write from the embedded set of core templates.
return writeEmbedded(runtime, template, dest)
}
func isCustom(template string) bool {
return len(strings.Split(template, "/")) > 1
func writeRemote(url, runtime, template, dest string) error {
// Clone a minimal copy of the remote repository in-memory.
r, err := git.Clone(
memory.NewStorage(),
memfs.New(),
&git.CloneOptions{
URL: url,
Depth: 1,
Tags: git.NoTags,
RecurseSubmodules: git.NoRecurseSubmodules,
})
if err != nil {
return err
}
wt, err := r.Worktree()
if err != nil {
return err
}
fs := wt.Filesystem
if _, err := fs.Stat(runtime); err != nil {
return ErrRuntimeNotFound
}
templatePath := filepath.Join(runtime, template)
if _, err := fs.Stat(templatePath); err != nil {
return ErrTemplateNotFound
}
accessor := billyFilesystem{fs: fs}
return copy(templatePath, dest, accessor)
}
func writeCustom(templatesPath, runtime, templateFullName, dest string) error {
if templatesPath == "" {
// write from a custom repository. The temlate full name is prefixed
func writeCustom(repositoriesPath, runtime, templateFullName, dest string) error {
// assert path to template repos provided
if repositoriesPath == "" {
return ErrRepositoriesNotDefined
}
if !repositoryExists(templatesPath, templateFullName) {
return ErrRepositoryNotFound
}
// ensure that the templateFullName is of the format "repoName/templateName"
// assert template in form "repoName/templateName"
cc := strings.Split(templateFullName, "/")
if len(cc) != 2 {
return ErrTemplateMissingRepository
}
repo := cc[0]
template := cc[1]
runtimePath := filepath.Join(templatesPath, repo, runtime)
_, err := os.Stat(runtimePath)
if err != nil {
var (
repo = cc[0]
template = cc[1]
repoPath = filepath.Join(repositoriesPath, repo)
runtimePath = filepath.Join(repositoriesPath, repo, runtime)
templatePath = filepath.Join(repositoriesPath, repo, runtime, template)
accessor = osFilesystem{} // in instanced provider of Stat and Open
)
if _, err := accessor.Stat(repoPath); err != nil {
return ErrRepositoryNotFound
}
if _, err := accessor.Stat(runtimePath); err != nil {
return ErrRuntimeNotFound
}
// Example FileSystem path:
// /home/alice/.config/func/templates/boson/go/json
templatePath := filepath.Join(templatesPath, repo, runtime, template)
_, err = os.Stat(templatePath)
if err != nil {
if _, err := accessor.Stat(templatePath); err != nil {
return ErrTemplateNotFound
}
return copy(templatePath, dest, filesystemAccessor{})
return copy(templatePath, dest, accessor)
}
func writeEmbedded(runtime, template, dest string) (err error) {
// Copy files to the destination
// Example embedded path:
// /templates/go/http
runtimePath := filepath.Join("/templates", runtime)
_, err = pkger.Stat(runtimePath)
if err != nil {
func writeEmbedded(runtime, template, dest string) error {
var (
repoPath = "/templates"
runtimePath = filepath.Join(repoPath, runtime)
templatePath = filepath.Join(repoPath, runtime, template)
accessor = pkgerFilesystem{} // instanced provder of Stat and Open
)
if _, err := accessor.Stat(runtimePath); err != nil {
return ErrRuntimeNotFound
}
templatePath := filepath.Join("/templates", runtime, template)
_, err = pkger.Stat(templatePath)
if err != nil {
if _, err := accessor.Stat(templatePath); err != nil {
return ErrTemplateNotFound
}
return copy(templatePath, dest, embeddedAccessor{})
return copy(templatePath, dest, accessor)
}
type embeddedAccessor struct{}
func (a embeddedAccessor) Stat(path string) (os.FileInfo, error) {
return pkger.Stat(path)
}
func (a embeddedAccessor) Open(path string) (file, error) {
return pkger.Open(path)
}
type filesystemAccessor struct{}
func (a filesystemAccessor) Stat(path string) (os.FileInfo, error) {
return os.Stat(path)
}
func (a filesystemAccessor) Open(path string) (file, error) {
return os.Open(path)
}
func repositoryExists(repositories, template string) bool {
cc := strings.Split(template, "/")
_, err := os.Stat(filepath.Join(repositories, cc[0]))
return err == nil
}
func copy(src, dest string, accessor fileAccessor) (err error) {
func copy(src, dest string, accessor filesystem) (err error) {
node, err := accessor.Stat(src)
if err != nil {
return
@ -169,7 +193,7 @@ func copy(src, dest string, accessor fileAccessor) (err error) {
}
}
func copyNode(src, dest string, accessor fileAccessor) (err error) {
func copyNode(src, dest string, accessor filesystem) (err error) {
node, err := accessor.Stat(src)
if err != nil {
return
@ -192,13 +216,8 @@ func copyNode(src, dest string, accessor fileAccessor) (err error) {
return
}
func readDir(src string, accessor fileAccessor) ([]os.FileInfo, error) {
f, err := accessor.Open(src)
if err != nil {
return nil, err
}
list, err := f.Readdir(-1)
f.Close()
func readDir(src string, accessor filesystem) ([]os.FileInfo, error) {
list, err := accessor.ReadDir(src)
if err != nil {
return nil, err
}
@ -206,7 +225,7 @@ func readDir(src string, accessor fileAccessor) ([]os.FileInfo, error) {
return list, nil
}
func copyLeaf(src, dest string, accessor fileAccessor) (err error) {
func copyLeaf(src, dest string, accessor filesystem) (err error) {
srcFile, err := accessor.Open(src)
if err != nil {
return
@ -227,3 +246,68 @@ func copyLeaf(src, dest string, accessor fileAccessor) (err error) {
_, err = io.Copy(destFile, srcFile)
return
}
// Filesystems
// Wrap the implementations of FS with their subtle differences into the
// common interface for accessing template files defined herein.
// os: standard for on-disk extensible template repositories.
// pker: embedded filesystem backed by the generated pkged.go.
// billy: go-git library's filesystem used for remote git template repos.
// osFilesystem is a template file accessor backed by an os
// filesystem.
type osFilesystem struct{}
func (a osFilesystem) Stat(path string) (os.FileInfo, error) {
return os.Stat(path)
}
func (a osFilesystem) Open(path string) (file, error) {
return os.Open(path)
}
func (a osFilesystem) ReadDir(path string) ([]os.FileInfo, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return f.Readdir(-1)
}
// pkgerFilesystem is template file accessor backed by the pkger-provided
// embedded filesystem.
type pkgerFilesystem struct{}
func (a pkgerFilesystem) Stat(path string) (os.FileInfo, error) {
return pkger.Stat(path)
}
func (a pkgerFilesystem) Open(path string) (file, error) {
return pkger.Open(path)
}
func (a pkgerFilesystem) ReadDir(path string) ([]os.FileInfo, error) {
f, err := pkger.Open(path)
if err != nil {
return nil, err
}
return f.Readdir(-1) // Really? Pkger's ReadDir is Readdir.
}
// billyFilesystem is a template file accessor backed by a billy FS
type billyFilesystem struct {
fs billy.Filesystem
}
func (a billyFilesystem) Stat(path string) (os.FileInfo, error) {
return a.fs.Stat(path)
}
func (a billyFilesystem) Open(path string) (file, error) {
return a.fs.Open(path)
}
func (a billyFilesystem) ReadDir(path string) ([]os.FileInfo, error) {
return a.fs.ReadDir(path)
}

View File

@ -4,6 +4,7 @@ package function
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
@ -42,7 +43,7 @@ func TestWriteCustom(t *testing.T) {
defer using(t, root)()
// Writer which includes reference to custom repositories location
w := templateWriter{templates: "testdata/repositories"}
w := templateWriter{repositories: "testdata/repositories"}
// template, in form [provider]/[template], on disk the template is
// located at testdata/repositories/[provider]/[runtime]/[template]
tpl := "customProvider/tpla"
@ -58,6 +59,41 @@ func TestWriteCustom(t *testing.T) {
}
}
// TestWriteRemote ensures that a Git template repository provided via URI
// can be specificed.
func TestWriteRemote(t *testing.T) {
// Create test directory
root := "testdata/testWriteRemote"
defer using(t, root)()
// The difference between HTTP vs File protocol is internal to the
// go-git library which implements the template writer. As such
// providing a local file URI is conceptually sufficient to test
// our usage, though in practice HTTP is expected to be the norm.
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
path := filepath.Join(cwd, "testdata", "repository.git")
url := fmt.Sprintf(`file://%s`, path)
t.Logf("cloning: %v", url)
// Create a writer which explicitly specifies the Git repo at URL
// rather than relying on the default internally builtin template repo
w := templateWriter{url: url}
err = w.Write("go", "remote", root)
if err != nil {
t.Fatal(err)
}
_, err = os.Stat(filepath.Join(root, "remote-test"))
if err != nil {
t.Fatal(err)
}
}
// TestWriteDefault ensures that the default template is used when not specified.
func TestWriteDefault(t *testing.T) {
// create test directory
@ -144,7 +180,7 @@ func TestWriteModeCustom(t *testing.T) {
defer using(t, root)()
// Write executable from custom repo
w := templateWriter{templates: "testdata/repositories"}
w := templateWriter{repositories: "testdata/repositories"}
err = w.Write(TestRuntime, "customProvider/tplb", root)
if err != nil {
t.Fatal(err)

23
testdata/README.md vendored
View File

@ -2,3 +2,26 @@
Contains test templates and directory targets for domain and subdomain-level tests.
## repositories
An example of an on-disk group of template repositories. Each
subdirectory is a single named repository. in practice these
would likely be Git repositories, but only the file structure
is expected: [repo name]/[language runtime]/[template name]
## repository.git
A bare git repository used to test specifying a repo directly.
Tests use a local file URI, but in practice this will likely
be specified as an HTTP URL.
### Initial Setup
- creating as a bare clone
- remove origin from `config`
- remove sample hooks
- touch a .gitinclude in refs/heads and refs/tags

1
testdata/repository.git/HEAD vendored Normal file
View File

@ -0,0 +1 @@
ref: refs/heads/main

6
testdata/repository.git/config vendored Normal file
View File

@ -0,0 +1,6 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true
ignorecase = true
precomposeunicode = true

1
testdata/repository.git/description vendored Normal file
View File

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

8
testdata/repository.git/hooks/post-update vendored Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
#
# An example hook script to prepare a packed repository for use over
# dumb transports.
#
# To enable this hook, rename this file to "post-update".
exec git update-server-info

6
testdata/repository.git/info/exclude vendored Normal file
View File

@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~

5
testdata/repository.git/info/refs vendored Normal file
View File

@ -0,0 +1,5 @@
<<<<<<< HEAD
9ac29c7ad8c06fa6613b794521568fbd7ea007dd refs/heads/main
=======
9868959b2b456ac7767bf351a84b369f9fbadb3a refs/heads/main
>>>>>>> 1e43021 (feat: remote template repositories)

View File

@ -0,0 +1,3 @@
xeRΛnά0 μY_Aμ9ΕΎ&η}
<04><>^%Ϋ΄MD<4D>…$gλΏ/)mΧ.jψ"r8ΩΫΠΓσσΣ§3Όα%$Κ!nJ}ωmάΕ"ΗςΛdL<64>€Ξgx]#΅RέB ψ7ο40<04> yς3Η'b>½g¦Λϊ® ‹”® Gθ7kΟ «§ O<>ƒ°NδΑτd)oζrΖόΥ9Εΰώc"L-ΐΧΑ<CE91><E280A6><EFBFBD><EFBFBD>K<EFBFBD>EwΜύ+RΖ:―.Ν€rB;<3B>"ϊΞάΞA·Jρμ?X <0B>#<23>LΑ+υ6\ΑΔΚ<CE94>2^nS‰jAI'•Q>)υ<>·`¬έκlσΚ Ϊ±s £Y΅Η+ΜlΒ<6C>“Ym†>?,Jk­$\,™΅iζBμynoΨ—΅+ψ"·*Φέζuο#Εcp<18>/i&δG&γGή9SΔρ•ΓΚϋέΫ”½Υf<rορ!8Η}§»Η%t<>mmπU<CF80>N
/u )Λ­}<7D><>ο]χZΠ¬Ώmαα΅Ψ7”<37>¦ιeG{uvlσ:^jΛSkΉP/Ϋύ0dMΟ·|ly[Σ©ήξιHt0¥ήΝΟ0<CE9F>΄±PΕίοI=ν

View File

@ -0,0 +1 @@

2
testdata/repository.git/packed-refs vendored Normal file
View File

@ -0,0 +1,2 @@
# pack-refs with: peeled fully-peeled sorted
9868959b2b456ac7767bf351a84b369f9fbadb3a refs/heads/main

View File

View File

@ -0,0 +1 @@
9ac29c7ad8c06fa6613b794521568fbd7ea007dd

View File