This is not a final patch. Following one of the comments from Thorben Günther regarding the git show command, I did a spike to show the license of a repository. The command to show the license is: go run . git show hut --license go run . git show hut -l If used with the "license" flag the git show command shows only the license, shipping all the other information. This allows to fetch the license and use the output to feed other scripts. I was able to get the LICENSE contents by defining the query licenseByRepositoryName. I had an issue with this query because the Go structure returned is an *Object and therefore I could not cast to a TextBlob to get the contents of the file. To make it work I changed manually the gql.go (I know I cannot do it) to have the Object field be a map[string]interface{} instead of *Object: type TreeEntry struct { Id string `json:"id"` Name string `json:"name"` Object map[string]interface{} `json:"object"` // change Mode int32 `json:"mode"` } Questions: 1. Is this feature usefull? 2. Is there a better way to get a file? 3. Should we change gqlclientgen to define a Go interface{} when the type in the schema.graphqls is an interface? 4. Do you see any other alternative? Renato Torres (1): git: show license git.go | 121 ++++++++++++++++++++------------ srht/gitsrht/gql.go | 20 ++++-- srht/gitsrht/operations.graphql | 14 ++++ 3 files changed, 105 insertions(+), 50 deletions(-) -- 2.34.1
Users can already do this with something like var raw json.RawMessage client.Execute(ctx, op, &raw) (See for instance the `hut graphql` command.) I'd prefer to add proper support for all GraphQL features, instead of providing additional ways for users to shoot themselves in the foot.
I was exploring your comments a bit further (and learned a lot about GraphQL...). If I understood correctly we cannot use "raw" as a generic solution because that's a specific field from the git.sr.ht GraphQL API.
We should be able to accommodate for name collisions. If we had an unexported "raw" field in the struct then it'll get ignored by encoding/json and an exported "Raw" field can co-exist.
I've hacked gqlclient to explore a solution for this. Consider the following schema ("stolen" from 99designs/gqlgen): ```graphql interface Character { # ... } type Human implements Character { # ... } type Droid implements Character { # ... } ``` Instead of generating: ```go type Character struct { // ... } type Human struct { // ... } type Droid struct { // ... } ``` We would generate: ```go type Character interface{}
Not a fan of this, because: - Character is an empty interface, so all types will satisfy it. - You can't do anything useful with just a Character, since it has no methods? - gqlclient would need to always know the concrete type of the Character to make it so users can type-switch. But below it seems like methods are added to Character, so not sure how this all ties together.
type CharacterFields struct { // ... } type Human struct { // ... } type Droid struct { // ... } ``` And the following methods: ```go func (i *Character) AsCharacterFields() *CharacterFields { bytes, _ := json.Marshal(i) charFields = new(CharacterFields) json.Unmarshal(bytes, &charFields) return charFields } func (i *Character) AsHuman() *Human { bytes, _ := json.Marshal(i) obj = new(Human) json.Unmarshal(bytes, &obj) return obj } // Etc.... ```
It's not possible to define methods on an interface type. And generating these requires knowing about all possible Character implementations?
The operations remain unchanged: ```go func Characters(client *gqlclient.Client, ctx context.Context) (characters []Character, err error) { op := gqlclient.NewOperation("query characters ... ") var respData struct { Characters []Character } err = client.Execute(ctx, op, &respData) return respData.Characters, err } ```
encoding/json is not able to unmarshal interface{} types.
I don't like very much the "Fields" suffix, eventually we would need to find a better naming schema. Additionally this has the drawback of having to use "AsCharacterFields" to access the fields declared in the GraphQL interface, something that we don't need with the current approach. > It would seem like this is a promise we can make for simple > cases, but not sure about federated GraphQL schemas and schemas split > into multiple files. Would need to do more research. I think with this approach we wouldn't have problems with other cases, because it does not require us to know about all implementations of a GraphQL interface at generation time (but I haven't done a proper research on this...). Example in cmd/gqlclientgen/main.go: ```go // ... func genDef(schema *ast.Schema, def *ast.Definition) *jen.Statement { switch def.Kind { // ... case ast.Object: var stmts []jen.Code var fields []jen.Code for _, field := range def.Fields { //... } for _, i := range def.Interfaces { interfaceName := i stmts = append(stmts, jen.Line()) stmts = append(stmts, jen.Func().Params(jen.Id("i").Op("*").Id(interfaceName)). Id("As"+def.Name).Params().Params(jen.Op("*"). Id(def.Name)).Block( jen.Id("bytes, _").Op(":="). Qual("encoding/json", "Marshal").Call(jen.Id("i")), jen.Id("obj").Op("=").New(jen.Id(def.Name)), jen.Qual("encoding/json", "Unmarshal").Call(jen.Id("bytes"), jen.Id("&obj")), jen.Return(jen.Id("obj")))) } // ... } ``` What do you think? I can send a gqlclient patch if you want to take a look at the code I used to generate this.
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~emersion/hut-dev/patches/28629/mbox | git am -3Learn more about email & git
--- git.go | 121 ++++++++++++++++++++------------ srht/gitsrht/gql.go | 20 ++++-- srht/gitsrht/operations.graphql | 14 ++++ 3 files changed, 105 insertions(+), 50 deletions(-) diff --git a/git.go b/git.go index cc5f18c..379da20 100644 --- a/git.go +++ b/git.go @@ -411,6 +411,7 @@ func newGitACLDeleteCommand() *cobra.Command { } func newGitShowCommand() *cobra.Command { + var showLicense bool run := func(cmd *cobra.Command, args []string) { ctx := cmd.Context() @@ -424,53 +425,12 @@ func newGitShowCommand() *cobra.Command { c := createClientWithInstance("git", cmd, instance) - repo, err := gitsrht.RepositoryByName(c.Client, ctx, name) - if err != nil { - log.Fatal(err) - } - - // prints basic information - fmt.Printf("%s (%s)\n", termfmt.Bold.String(repo.Name), repo.Visibility.TermString()) - if repo.Description != nil && *repo.Description != "" { - fmt.Printf(" %s\n", *repo.Description) - } - if repo.UpstreamUrl != nil && *repo.UpstreamUrl != "" { - fmt.Printf(" Upstream URL: %s\n", *repo.UpstreamUrl) - } - - // prints latest tag - tags := repo.References.Tags() - if len(tags) > 0 { - fmt.Println() - fmt.Printf(" Latest tag: %s\n", tags[0]) - } - - // prints branches - branches := repo.References.Heads() - if len(branches) > 0 { - fmt.Println() - fmt.Printf(" Branches:\n") - for i := 0; i < len(branches); i++ { - fmt.Printf(" %s\n", branches[i]) - } + if showLicense { + showRepositoryLicense(c, ctx, name) + return } - // prints the three most recent commits - if len(repo.Log.Results) >= 3 { - fmt.Println() - fmt.Printf(" Recent log:\n") - - for _, commit := range repo.Log.Results[:3] { - fmt.Printf(" %s %s <%s> (%s ago)\n", - termfmt.Yellow.Sprintf("%s", commit.ShortId), - commit.Author.Name, - commit.Author.Email, - timeDelta(commit.Author.Time)) - - commitLines := strings.Split(commit.Message, "\n") - fmt.Printf(" %s\n", commitLines[0]) - } - } + showRepository(c, ctx, name) } cmd := &cobra.Command{ @@ -481,9 +441,80 @@ func newGitShowCommand() *cobra.Command { Run: run, } + cmd.Flags().BoolVarP(&showLicense, "license", "l", false, "shows only the license") + return cmd } +func showRepositoryLicense(c *Client, ctx context.Context, name string) { + repo, err := gitsrht.LicenseByRepositoryName(c.Client, ctx, name) + if err != nil { + log.Fatal(err) + } + if repo == nil { + log.Fatalf("repository %s does not exist", name) + } + + if repo.Path == nil { + log.Fatalf("LICENSE file not found for %s", name) + } + license := repo.Path.Object["text"] + + fmt.Println(license) +} + +func showRepository(c *Client, ctx context.Context, name string) { + repo, err := gitsrht.RepositoryByName(c.Client, ctx, name) + if err != nil { + log.Fatal(err) + } + if repo == nil { + log.Fatalf("repository %s does not exist", name) + } + + // prints basic information + if repo.Description != nil && *repo.Description != "" { + fmt.Printf(" %s\n", *repo.Description) + } + if repo.UpstreamUrl != nil && *repo.UpstreamUrl != "" { + fmt.Printf(" Upstream URL: %s\n", *repo.UpstreamUrl) + } + + // prints latest tag + tags := repo.References.Tags() + if len(tags) > 0 { + fmt.Println() + fmt.Printf(" Latest tag: %s\n", tags[0]) + } + + // prints branches + branches := repo.References.Heads() + if len(branches) > 0 { + fmt.Println() + fmt.Printf(" Branches:\n") + for i := 0; i < len(branches); i++ { + fmt.Printf(" %s\n", branches[i]) + } + } + + // prints the three most recent commits + if len(repo.Log.Results) >= 3 { + fmt.Println() + fmt.Printf(" Recent log:\n") + + for _, commit := range repo.Log.Results[:3] { + fmt.Printf(" %s %s <%s> (%s ago)\n", + termfmt.Yellow.Sprintf("%s", commit.ShortId), + commit.Author.Name, + commit.Author.Email, + timeDelta(commit.Author.Time)) + + commitLines := strings.Split(commit.Message, "\n") + fmt.Printf(" %s\n", commitLines[0]) + } + } +} + func getRepoName(ctx context.Context, cmd *cobra.Command) (repoName, instance string) { if repoName, err := cmd.Flags().GetString("repo"); err != nil { log.Fatal(err) diff --git a/srht/gitsrht/gql.go b/srht/gitsrht/gql.go index 55d59ea..ac225a4 100644 --- a/srht/gitsrht/gql.go +++ b/srht/gitsrht/gql.go @@ -205,10 +205,10 @@ type Tree struct { } type TreeEntry struct { - Id string `json:"id"` - Name string `json:"name"` - Object *Object `json:"object"` - Mode int32 `json:"mode"` + Id string `json:"id"` + Name string `json:"name"` + Object map[string]interface {} `json:"object"` + Mode int32 `json:"mode"` } type TreeEntryCursor struct { @@ -267,7 +267,17 @@ func ListArtifacts(client *gqlclient.Client, ctx context.Context, name string) ( } func RepositoryByName(client *gqlclient.Client, ctx context.Context, name string) (repositoryByName *Repository, err error) { - op := gqlclient.NewOperation("query repositoryByName ($name: String!) {\n\trepositoryByName(name: $name) {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t\tupstreamUrl\n\t\treferences {\n\t\t\tresults {\n\t\t\t\tname\n\t\t\t}\n\t\t}\n\t\tlog {\n\t\t\tresults {\n\t\t\t\tshortId\n\t\t\t\tauthor {\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t\ttime\n\t\t\t\t}\n\t\t\t\tmessage\n\t\t\t}\n\t\t}\n\t}\n}\n") + op := gqlclient.NewOperation("query repositoryByName ($name: String!) {\n\trepositoryByName(name: $name) {\n\t\tname\n\t\tdescription\n\t\tvisibility\n\t\tupstreamUrl\n\t\tpath(path: \"LICENSE\") {\n\t\t\tname\n\t\t\tobject {\n\t\t\t\t... on TextBlob {\n\t\t\t\t\ttext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treferences {\n\t\t\tresults {\n\t\t\t\tname\n\t\t\t}\n\t\t}\n\t\tlog {\n\t\t\tresults {\n\t\t\t\tshortId\n\t\t\t\tauthor {\n\t\t\t\t\tname\n\t\t\t\t\temail\n\t\t\t\t\ttime\n\t\t\t\t}\n\t\t\t\tmessage\n\t\t\t}\n\t\t}\n\t}\n}\n") + op.Var("name", name) + var respData struct { + RepositoryByName *Repository + } + err = client.Execute(ctx, op, &respData) + return respData.RepositoryByName, err +} + +func LicenseByRepositoryName(client *gqlclient.Client, ctx context.Context, name string) (repositoryByName *Repository, err error) { + op := gqlclient.NewOperation("query licenseByRepositoryByName ($name: String!) {\n\trepositoryByName(name: $name) {\n\t\tpath(path: \"LICENSE\") {\n\t\t\tname\n\t\t\tobject {\n\t\t\t\t... on TextBlob {\n\t\t\t\t\ttext\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n") op.Var("name", name) var respData struct { RepositoryByName *Repository diff --git a/srht/gitsrht/operations.graphql b/srht/gitsrht/operations.graphql index 57d2cfd..3ec5ad1 100644 --- a/srht/gitsrht/operations.graphql +++ b/srht/gitsrht/operations.graphql @@ -45,6 +45,20 @@ query repositoryByName($name: String!) { } } + +query licenseByRepositoryName($name: String!) { + repositoryByName(name: $name) { + path(path: "LICENSE") { + name + object { + ... on TextBlob { + text + } + } + } + } +} + query repositories { repositories { ...repos -- 2.34.1