~emersion/hut-dev

git: show license spike v1 NEEDS REVISION

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.
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{}
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....
```
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
}

```
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.
Export patchset (mbox)
How do I use 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 -3
Learn more about email & git

[PATCH v1 1/1] git: show license Export this patch

---
 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