~emersion/hut

783eee5fcfe01b18ecaeec06a5488094d96ea3ca — delthas 5 months ago 4483163
export: Support specifying services and resources

This adds support for exporting specific resources or services, by
passing their URL to hut export.

This also implicitly adds support for exporting resources of other
users, by explicitly specifying the URL of a resource they own.

The same output tree is kept: $out/$service/$name. This means that
importing two repos of the same name by different users will not
work, but it makes sense: when importing, we cannot create those
two resources with the same name.
M doc/hut.1.scd => doc/hut.1.scd +5 -1
@@ 83,9 83,13 @@ Additionally, mailing lists can be referred to by their email address.
*init*
	Initialize hut's configuration file.

*export* <directory>
*export* <directory> [resource|service...]
	Export account data.

	By default, all data of the current user will be exported. Alternatively,
	an explicit list of instance services (e.g. "todo.sr.ht") or resources
	(e.g. "todo.sr.ht/~user/tracker") can be specified.

*import* <directory>
	Import account data.


M export.go => export.go +82 -23
@@ 1,10 1,13 @@
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"path"
	"strings"
	"time"

	"github.com/spf13/cobra"


@@ 28,8 31,6 @@ func newExportCommand() *cobra.Command {
	run := func(cmd *cobra.Command, args []string) {
		var exporters []exporter

		// TODO: Allow exporting a subset of all services (maybe meta should
		// provide a list of services configured for that instance?)
		mc := createClient("meta", cmd)
		meta := export.NewMetaExporter(mc.Client)
		exporters = append(exporters, exporter{meta, "meta.sr.ht", mc.BaseURL})


@@ 66,47 67,96 @@ func newExportCommand() *cobra.Command {
		ctx := cmd.Context()
		log.Println("Exporting account data...")

		for _, ex := range exporters {
			log.Println(ex.Name)
		out := args[0]
		resources := args[1:]

			base := path.Join(args[0], ex.Name)
			if err := os.MkdirAll(base, 0o755); err != nil {
				log.Fatalf("Failed to create export directory: %s", err.Error())
		// Export all services by default
		if len(resources) == 0 {
			for _, ex := range exporters {
				resources = append(resources, ex.BaseURL)
			}
		}

		for _, resource := range resources {
			log.Println(resource)

			stamp := path.Join(base, "service.json")
			if _, err := os.Stat(stamp); err == nil {
				log.Printf("Skipping %s (already exported)", ex.Name)
				continue
			var name, owner, instance string
			if res := stripProtocol(resource); !strings.Contains(res, "/") {
				instance = res
			} else {
				name, owner, instance = parseResourceName(resource)
				owner = strings.TrimLeft(owner, ownerPrefixes)
			}

			if err := ex.Export(ctx, base); err != nil {
				log.Printf("Error exporting %s: %s", ex.Name, err.Error())
				continue
			var ex *exporter
			for _, e := range exporters {
				if stripProtocol(e.BaseURL) == instance {
					ex = &e
					break
				}
			}
			if ex == nil {
				log.Fatalf("Unknown resource instance: %s", resource)
			}

			info := ExportInfo{
				Instance: ex.BaseURL,
				Service:  ex.Name,
				Date:     time.Now().UTC(),
			var err error
			if name == "" && owner == "" {
				err = exportService(ctx, out, ex)
			} else if name != "" && owner != "" {
				err = exportResource(ctx, out, ex, owner, name)
			} else {
				err = fmt.Errorf("unknown resource")
			}
			if err := writeExportStamp(stamp, &info); err != nil {
				log.Printf("Error writing stamp for %s: %s", ex.Name, err.Error())
			if err != nil {
				log.Printf("Failed to export %q: %v", resource, err)
			}
		}

		log.Println("Export complete.")
	}
	return &cobra.Command{
		Use:   "export <directory>",
		Use:   "export <directory> [resource|service...]",
		Short: "Exports your account data",
		Args:  cobra.ExactArgs(1),
		Args:  cobra.MinimumNArgs(1),
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			return nil, cobra.ShellCompDirectiveFilterDirs
			if len(args) <= 1 {
				return nil, cobra.ShellCompDirectiveFilterDirs
			}
			// TODO: completion on export resources
			return nil, cobra.ShellCompDirectiveNoFileComp
		},
		Run: run,
	}
}

func exportService(ctx context.Context, out string, ex *exporter) error {
	base := path.Join(out, ex.Name)
	if err := os.MkdirAll(base, 0o755); err != nil {
		return fmt.Errorf("failed to create export directory: %v", err)
	}

	stamp := path.Join(base, "service.json")
	if _, err := os.Stat(stamp); err == nil {
		log.Printf("Skipping %s (already exported)", ex.Name)
		return nil
	}

	if err := ex.Export(ctx, base); err != nil {
		return err
	}

	info := ExportInfo{
		Instance: ex.BaseURL,
		Service:  ex.Name,
		Date:     time.Now().UTC(),
	}
	if err := writeExportStamp(stamp, &info); err != nil {
		return fmt.Errorf("failed writing stamp: %v", err)
	}

	return nil
}

func writeExportStamp(path string, info *ExportInfo) error {
	file, err := os.Create(path)
	if err != nil {


@@ 116,3 166,12 @@ func writeExportStamp(path string, info *ExportInfo) error {

	return json.NewEncoder(file).Encode(info)
}

func exportResource(ctx context.Context, out string, ex *exporter, owner, name string) error {
	base := path.Join(out, ex.Name, name)
	if err := os.MkdirAll(base, 0o755); err != nil {
		return fmt.Errorf("failed to create export directory: %v", err)
	}

	return ex.ExportResource(ctx, base, owner, name)
}

M export/builds.go => export/builds.go +14 -0
@@ 10,6 10,7 @@ import (
	"os"
	"path"
	"strconv"
	"strings"
	"time"

	"git.sr.ht/~emersion/gqlclient"


@@ 79,6 80,19 @@ func (ex *BuildsExporter) Export(ctx context.Context, dir string) error {
	return ret
}

func (ex *BuildsExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
	resource = strings.TrimPrefix(resource, "job/")
	id, err := strconv.ParseInt(resource, 10, 64)
	if err != nil {
		return fmt.Errorf("failed to parse builds resource %v: %v", resource, err)
	}
	job, err := buildssrht.ExportJob(ex.client, ctx, int32(id))
	if err != nil {
		return err
	}
	return ex.exportJob(ctx, job, dir)
}

func (ex *BuildsExporter) exportJob(ctx context.Context, job *buildssrht.Job, base string) error {
	infoPath := path.Join(base, infoFilename)
	if _, err := os.Stat(infoPath); err == nil {

M export/git.go => export/git.go +71 -52
@@ 18,12 18,16 @@ import (
const gitRepositoryDir = "repository.git"

type GitExporter struct {
	client  *gqlclient.Client
	baseURL string
	client       *gqlclient.Client
	baseURL      string
	baseCloneURL string
}

func NewGitExporter(client *gqlclient.Client, baseURL string) *GitExporter {
	return &GitExporter{client, baseURL}
	return &GitExporter{
		client:  client,
		baseURL: baseURL,
	}
}

// A subset of gitsrht.Repository which only contains the fields we want to


@@ 37,17 41,6 @@ type GitRepoInfo struct {
}

func (ex *GitExporter) Export(ctx context.Context, dir string) error {
	settings, err := gitsrht.SshSettings(ex.client, ctx)
	if err != nil {
		return err
	}
	sshUser := settings.Settings.SshUser

	baseURL, err := url.Parse(ex.baseURL)
	if err != nil {
		panic(err)
	}

	var cursor *gitsrht.Cursor
	for {
		repos, err := gitsrht.ExportRepositories(ex.client, ctx, cursor)


@@ 55,44 48,9 @@ func (ex *GitExporter) Export(ctx context.Context, dir string) error {
			return err
		}

		// TODO: Should we fetch & store ACLs?
		for _, repo := range repos.Results {
			repoPath := path.Join(dir, repo.Name)
			infoPath := path.Join(repoPath, infoFilename)
			clonePath := path.Join(repoPath, gitRepositoryDir)
			cloneURL := fmt.Sprintf("%s@%s:%s/%s", sshUser, baseURL.Host, repo.Owner.CanonicalName, repo.Name)

			if _, err := os.Stat(clonePath); err == nil {
				log.Printf("\tSkipping %s (already exists)", repo.Name)
				continue
			}
			if err := os.MkdirAll(repoPath, 0o755); err != nil {
				return err
			}

			log.Printf("\tCloning %s", repo.Name)
			cmd := exec.Command("git", "clone", "--mirror", cloneURL, clonePath)
			if err := cmd.Run(); err != nil {
				return err
			}

			var head *string
			if repo.HEAD != nil {
				h := strings.TrimPrefix(repo.HEAD.Name, "refs/heads/")
				head = &h
			}

			repoInfo := GitRepoInfo{
				Info: Info{
					Service: "git.sr.ht",
					Name:    repo.Name,
				},
				Description: repo.Description,
				Visibility:  repo.Visibility,
				Readme:      repo.Readme,
				Head:        head,
			}
			if err := writeJSON(infoPath, &repoInfo); err != nil {
			base := path.Join(dir, repo.Name)
			if err := ex.exportRepository(ctx, &repo, base); err != nil {
				return err
			}
		}


@@ 102,10 60,71 @@ func (ex *GitExporter) Export(ctx context.Context, dir string) error {
			break
		}
	}

	return nil
}

func (ex *GitExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
	user, err := gitsrht.ExportRepository(ex.client, ctx, owner, resource)
	if err != nil {
		return err
	}
	return ex.exportRepository(ctx, user.Repository, dir)
}

func (ex *GitExporter) exportRepository(ctx context.Context, repo *gitsrht.Repository, base string) error {
	// Cache base clone URL in exporter.
	if ex.baseCloneURL == "" {
		settings, err := gitsrht.SshSettings(ex.client, ctx)
		if err != nil {
			return err
		}
		sshUser := settings.Settings.SshUser

		baseURL, err := url.Parse(ex.baseURL)
		if err != nil {
			panic(err)
		}
		ex.baseCloneURL = fmt.Sprintf("%s@%s", sshUser, baseURL.Host)
	}

	// TODO: Should we fetch & store ACLs?
	infoPath := path.Join(base, infoFilename)
	clonePath := path.Join(base, gitRepositoryDir)
	cloneURL := fmt.Sprintf("%s:%s/%s", ex.baseCloneURL, repo.Owner.CanonicalName, repo.Name)

	if _, err := os.Stat(clonePath); err == nil {
		log.Printf("\tSkipping %s (already exists)", repo.Name)
		return nil
	}
	if err := os.MkdirAll(base, 0o755); err != nil {
		return err
	}

	log.Printf("\tCloning %s", repo.Name)
	cmd := exec.CommandContext(ctx, "git", "clone", "--mirror", cloneURL, clonePath)
	if err := cmd.Run(); err != nil {
		return err
	}

	var head *string
	if repo.HEAD != nil {
		h := strings.TrimPrefix(repo.HEAD.Name, "refs/heads/")
		head = &h
	}

	repoInfo := GitRepoInfo{
		Info: Info{
			Service: "git.sr.ht",
			Name:    repo.Name,
		},
		Description: repo.Description,
		Visibility:  repo.Visibility,
		Readme:      repo.Readme,
		Head:        head,
	}
	return writeJSON(infoPath, &repoInfo)
}

func (ex *GitExporter) ImportResource(ctx context.Context, dir string) error {
	settings, err := gitsrht.SshSettings(ex.client, ctx)
	if err != nil {

M export/hg.go => export/hg.go +47 -37
@@ 36,11 36,6 @@ type HgRepoInfo struct {
}

func (ex *HgExporter) Export(ctx context.Context, dir string) error {
	baseURL, err := url.Parse(ex.baseURL)
	if err != nil {
		panic(err)
	}

	var cursor *hgsrht.Cursor
	for {
		repos, err := hgsrht.ExportRepositories(ex.client, ctx, cursor)


@@ 48,39 43,9 @@ func (ex *HgExporter) Export(ctx context.Context, dir string) error {
			return err
		}

		// TODO: Should we fetch & store ACLs?
		for _, repo := range repos.Results {
			repoPath := path.Join(dir, repo.Name)
			infoPath := path.Join(repoPath, infoFilename)
			clonePath := path.Join(repoPath, hgRepositoryDir)
			cloneURL := fmt.Sprintf("ssh://hg@%s/%s/%s", baseURL.Host, repo.Owner.CanonicalName, repo.Name)

			if _, err := os.Stat(clonePath); err == nil {
				log.Printf("\tSkipping %s (already exists)", repo.Name)
				continue
			}
			if err := os.MkdirAll(repoPath, 0o755); err != nil {
				return err
			}

			log.Printf("\tCloning %s", repo.Name)
			cmd := exec.Command("hg", "clone", "-U", cloneURL, clonePath)
			err := cmd.Run()
			if err != nil {
				return err
			}

			repoInfo := HgRepoInfo{
				Info: Info{
					Service: "hg.sr.ht",
					Name:    repo.Name,
				},
				Description:   repo.Description,
				Visibility:    repo.Visibility,
				Readme:        repo.Readme,
				NonPublishing: repo.NonPublishing,
			}
			if err := writeJSON(infoPath, &repoInfo); err != nil {
			base := path.Join(dir, repo.Name)
			if err := ex.exportRepository(ctx, repo, base); err != nil {
				return err
			}
		}


@@ 94,6 59,51 @@ func (ex *HgExporter) Export(ctx context.Context, dir string) error {
	return nil
}

func (ex *HgExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
	user, err := hgsrht.ExportRepository(ex.client, ctx, owner, resource)
	if err != nil {
		return err
	}
	return ex.exportRepository(ctx, user.Repository, dir)
}

func (ex *HgExporter) exportRepository(ctx context.Context, repo *hgsrht.Repository, base string) error {
	// TODO: Should we fetch & store ACLs?
	baseURL, err := url.Parse(ex.baseURL)
	if err != nil {
		panic(err)
	}
	infoPath := path.Join(base, infoFilename)
	clonePath := path.Join(base, hgRepositoryDir)
	cloneURL := fmt.Sprintf("ssh://hg@%s/%s/%s", baseURL.Host, repo.Owner.CanonicalName, repo.Name)

	if _, err := os.Stat(clonePath); err == nil {
		log.Printf("\tSkipping %s (already exists)", repo.Name)
		return nil
	}
	if err := os.MkdirAll(base, 0o755); err != nil {
		return err
	}

	log.Printf("\tCloning %s", repo.Name)
	cmd := exec.CommandContext(ctx, "hg", "clone", "-U", cloneURL, clonePath)
	if err := cmd.Run(); err != nil {
		return err
	}

	repoInfo := HgRepoInfo{
		Info: Info{
			Service: "hg.sr.ht",
			Name:    repo.Name,
		},
		Description:   repo.Description,
		Visibility:    repo.Visibility,
		Readme:        repo.Readme,
		NonPublishing: repo.NonPublishing,
	}
	return writeJSON(infoPath, &repoInfo)
}

func (ex *HgExporter) ImportResource(ctx context.Context, dir string) error {
	baseURL, err := url.Parse(ex.baseURL)
	if err != nil {

M export/iface.go => export/iface.go +1 -0
@@ 17,6 17,7 @@ type Info struct {

type Exporter interface {
	Export(ctx context.Context, dir string) error
	ExportResource(ctx context.Context, dir, owner, name string) error
	ImportResource(ctx context.Context, dir string) error
}


M export/lists.go => export/lists.go +10 -2
@@ 63,7 63,7 @@ func (ex *ListsExporter) Export(ctx context.Context, dir string) error {
				return err
			}

			if err := ex.exportList(ctx, list, base); err != nil {
			if err := ex.exportList(ctx, &list, base); err != nil {
				var pe partialError
				if errors.As(err, &pe) {
					ret = err


@@ 82,7 82,15 @@ func (ex *ListsExporter) Export(ctx context.Context, dir string) error {
	return ret
}

func (ex *ListsExporter) exportList(ctx context.Context, list listssrht.MailingList, base string) error {
func (ex *ListsExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
	user, err := listssrht.ExportMailingList(ex.client, ctx, owner, resource)
	if err != nil {
		return err
	}
	return ex.exportList(ctx, user.List, dir)
}

func (ex *ListsExporter) exportList(ctx context.Context, list *listssrht.MailingList, base string) error {
	infoPath := path.Join(base, infoFilename)
	if _, err := os.Stat(infoPath); err == nil {
		log.Printf("\tSkipping %s (already exists)", list.Name)

M export/meta.go => export/meta.go +4 -0
@@ 96,6 96,10 @@ func (ex *MetaExporter) Export(ctx context.Context, dir string) error {
	return nil
}

func (ex *MetaExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
	return fmt.Errorf("exporting individual meta resources is not supported")
}

func (ex *MetaExporter) ImportResource(ctx context.Context, dir string) error {
	sshFile, err := os.Open(path.Join(dir, sshKeysFilename))
	if err != nil {

M export/paste.go => export/paste.go +11 -3
@@ 49,7 49,8 @@ func (ex *PasteExporter) Export(ctx context.Context, dir string) error {
		}

		for _, paste := range pastes.Results {
			if err := ex.exportPaste(ctx, &paste, dir); err != nil {
			base := path.Join(dir, paste.Id)
			if err := ex.exportPaste(ctx, &paste, base); err != nil {
				var pe partialError
				if errors.As(err, &pe) {
					ret = err


@@ 68,8 69,15 @@ func (ex *PasteExporter) Export(ctx context.Context, dir string) error {
	return ret
}

func (ex *PasteExporter) exportPaste(ctx context.Context, paste *pastesrht.Paste, dir string) error {
	base := path.Join(dir, paste.Id)
func (ex *PasteExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
	paste, err := pastesrht.PasteContentsByID(ex.client, ctx, resource)
	if err != nil {
		return err
	}
	return ex.exportPaste(ctx, paste, dir)
}

func (ex *PasteExporter) exportPaste(ctx context.Context, paste *pastesrht.Paste, base string) error {
	infoPath := path.Join(base, infoFilename)
	if _, err := os.Stat(infoPath); err == nil {
		log.Printf("\tSkipping %s (already exists)", paste.Id)

M export/todo.go => export/todo.go +10 -3
@@ 51,8 51,7 @@ func (ex *TodoExporter) Export(ctx context.Context, dir string) error {

		for _, tracker := range trackers.Results {
			base := path.Join(dir, tracker.Name)

			if err := ex.exportTracker(ctx, tracker, base); err != nil {
			if err := ex.exportTracker(ctx, &tracker, base); err != nil {
				var pe partialError
				if errors.As(err, &pe) {
					ret = err


@@ 71,7 70,15 @@ func (ex *TodoExporter) Export(ctx context.Context, dir string) error {
	return ret
}

func (ex *TodoExporter) exportTracker(ctx context.Context, tracker todosrht.Tracker, base string) error {
func (ex *TodoExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
	user, err := todosrht.ExportTracker(ex.client, ctx, owner, resource)
	if err != nil {
		return err
	}
	return ex.exportTracker(ctx, user.Tracker, dir)
}

func (ex *TodoExporter) exportTracker(ctx context.Context, tracker *todosrht.Tracker, base string) error {
	infoPath := path.Join(base, infoFilename)
	if _, err := os.Stat(infoPath); err == nil {
		log.Printf("\tSkipping %s (already exists)", tracker.Name)

M srht/buildssrht/gql.go => srht/buildssrht/gql.go +11 -1
@@ 635,8 635,18 @@ func JobsByUser(client *gqlclient.Client, ctx context.Context, username string, 
	return respData.UserByName, err
}

func ExportJob(client *gqlclient.Client, ctx context.Context, id int32) (job *Job, err error) {
	op := gqlclient.NewOperation("query exportJob ($id: Int!) {\n\tjob(id: $id) {\n\t\t... jobExport\n\t}\n}\nfragment jobExport on Job {\n\tid\n\tstatus\n\tnote\n\ttags\n\tvisibility\n\tlog {\n\t\tfullURL\n\t}\n\ttasks {\n\t\tname\n\t\tstatus\n\t\tlog {\n\t\t\tfullURL\n\t\t}\n\t}\n}\n")
	op.Var("id", id)
	var respData struct {
		Job *Job
	}
	err = client.Execute(ctx, op, &respData)
	return respData.Job, err
}

func ExportJobs(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (jobs *JobCursor, err error) {
	op := gqlclient.NewOperation("query exportJobs ($cursor: Cursor) {\n\tjobs(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tstatus\n\t\t\tnote\n\t\t\ttags\n\t\t\tvisibility\n\t\t\tlog {\n\t\t\t\tfullURL\n\t\t\t}\n\t\t\ttasks {\n\t\t\t\tname\n\t\t\t\tstatus\n\t\t\t\tlog {\n\t\t\t\t\tfullURL\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n")
	op := gqlclient.NewOperation("query exportJobs ($cursor: Cursor) {\n\tjobs(cursor: $cursor) {\n\t\tresults {\n\t\t\t... jobExport\n\t\t}\n\t\tcursor\n\t}\n}\nfragment jobExport on Job {\n\tid\n\tstatus\n\tnote\n\ttags\n\tvisibility\n\tlog {\n\t\tfullURL\n\t}\n\ttasks {\n\t\tname\n\t\tstatus\n\t\tlog {\n\t\t\tfullURL\n\t\t}\n\t}\n}\n")
	op.Var("cursor", cursor)
	var respData struct {
		Jobs *JobCursor

M srht/buildssrht/operations.graphql => srht/buildssrht/operations.graphql +25 -15
@@ 99,29 99,39 @@ fragment jobs on JobCursor {
    cursor
}

query exportJob($id: Int!) {
    job(id: $id) {
        ...jobExport
    }
}

query exportJobs($cursor: Cursor) {
    jobs(cursor: $cursor) {
        results {
            id
            status
            note
            tags
            visibility
            log {
                fullURL
            }
            tasks {
                name
                status
                log {
                    fullURL
                }
            }
            ...jobExport
        }
        cursor
    }
}

fragment jobExport on Job {
    id
    status
    note
    tags
    visibility
    log {
        fullURL
    }
    tasks {
        name
        status
        log {
            fullURL
        }
    }
}

query show($id: Int!) {
    job(id: $id) {
        id

M srht/gitsrht/gql.go => srht/gitsrht/gql.go +12 -1
@@ 675,8 675,19 @@ func RepositoriesByUser(client *gqlclient.Client, ctx context.Context, username 
	return respData.User, err
}

func ExportRepository(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) {
	op := gqlclient.NewOperation("query exportRepository ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\t... repositoryExport\n\t\t}\n\t}\n}\nfragment repositoryExport on Repository {\n\tname\n\towner {\n\t\tcanonicalName\n\t}\n\tdescription\n\tvisibility\n\treadme\n\tHEAD {\n\t\tname\n\t}\n}\n")
	op.Var("username", username)
	op.Var("name", name)
	var respData struct {
		User *User
	}
	err = client.Execute(ctx, op, &respData)
	return respData.User, err
}

func ExportRepositories(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (repositories *RepositoryCursor, err error) {
	op := gqlclient.NewOperation("query exportRepositories ($cursor: Cursor) {\n\trepositories(cursor: $cursor) {\n\t\tresults {\n\t\t\tname\n\t\t\towner {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t\tdescription\n\t\t\tvisibility\n\t\t\treadme\n\t\t\tHEAD {\n\t\t\t\tname\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n")
	op := gqlclient.NewOperation("query exportRepositories ($cursor: Cursor) {\n\trepositories(cursor: $cursor) {\n\t\tresults {\n\t\t\t... repositoryExport\n\t\t}\n\t\tcursor\n\t}\n}\nfragment repositoryExport on Repository {\n\tname\n\towner {\n\t\tcanonicalName\n\t}\n\tdescription\n\tvisibility\n\treadme\n\tHEAD {\n\t\tname\n\t}\n}\n")
	op.Var("cursor", cursor)
	var respData struct {
		Repositories *RepositoryCursor

M srht/gitsrht/operations.graphql => srht/gitsrht/operations.graphql +22 -10
@@ 108,24 108,36 @@ fragment repos on RepositoryCursor {
    cursor
}

query exportRepository($username: String!, $name: String!) {
    user(username: $username) {
        repository(name: $name) {
            ...repositoryExport
        }
    }
}

query exportRepositories($cursor: Cursor) {
    repositories(cursor: $cursor) {
        results {
            name
            owner {
                canonicalName
            }
            description
            visibility
            readme
            HEAD {
                name
            }
            ...repositoryExport
        }
        cursor
    }
}

fragment repositoryExport on Repository {
    name
    owner {
        canonicalName
    }
    description
    visibility
    readme
    HEAD {
        name
    }
}

query sshSettings {
    version {
        settings {

M srht/hgsrht/gql.go => srht/hgsrht/gql.go +13 -2
@@ 439,8 439,19 @@ func RepositoriesByUser(client *gqlclient.Client, ctx context.Context, username 
	return respData.User, err
}

func ExportRepository(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) {
	op := gqlclient.NewOperation("query exportRepository ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\trepository(name: $name) {\n\t\t\t... repositoryExport\n\t\t}\n\t}\n}\nfragment repositoryExport on Repository {\n\tname\n\towner {\n\t\tcanonicalName\n\t}\n\tdescription\n\tvisibility\n\treadme\n\tnonPublishing\n}\n")
	op.Var("username", username)
	op.Var("name", name)
	var respData struct {
		User *User
	}
	err = client.Execute(ctx, op, &respData)
	return respData.User, err
}

func ExportRepositories(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (repositories *RepositoryCursor, err error) {
	op := gqlclient.NewOperation("query exportRepositories ($cursor: Cursor) {\n\trepositories(cursor: $cursor) {\n\t\tresults {\n\t\t\tname\n\t\t\towner {\n\t\t\t\tcanonicalName\n\t\t\t}\n\t\t\tdescription\n\t\t\tvisibility\n\t\t\treadme\n\t\t\tnonPublishing\n\t\t}\n\t\tcursor\n\t}\n}\n")
	op := gqlclient.NewOperation("query exportRepositories ($cursor: Cursor) {\n\trepositories(cursor: $cursor) {\n\t\tresults {\n\t\t\t... repositoryExport\n\t\t}\n\t\tcursor\n\t}\n}\nfragment repositoryExport on Repository {\n\tname\n\towner {\n\t\tcanonicalName\n\t}\n\tdescription\n\tvisibility\n\treadme\n\tnonPublishing\n}\n")
	op.Var("cursor", cursor)
	var respData struct {
		Repositories *RepositoryCursor


@@ 460,7 471,7 @@ func UserWebhooks(client *gqlclient.Client, ctx context.Context, cursor *Cursor)
}

func CreateRepository(client *gqlclient.Client, ctx context.Context, name string, visibility Visibility, description string) (createRepository *Repository, err error) {
	op := gqlclient.NewOperation("mutation createRepository ($name: String!, $visibility: Visibility!, $description: String!) {\n\tcreateRepository(name: $name, visibility: $visibility, description: $description) {\n\t\tname\n\t}\n}\n")
	op := gqlclient.NewOperation("mutation createRepository ($name: String!, $visibility: Visibility!, $description: String!) {\n\tcreateRepository(name: $name, visibility: $visibility, description: $description) {\n\t\tid\n\t\tname\n\t\towner {\n\t\t\tcanonicalName\n\t\t}\n\t}\n}\n")
	op.Var("name", name)
	op.Var("visibility", visibility)
	op.Var("description", description)

M srht/hgsrht/operations.graphql => srht/hgsrht/operations.graphql +20 -8
@@ 40,22 40,34 @@ fragment repos on RepositoryCursor {
    cursor
}

query exportRepository($username: String!, $name: String!) {
    user(username: $username) {
        repository(name: $name) {
            ...repositoryExport
        }
    }
}

query exportRepositories($cursor: Cursor) {
    repositories(cursor: $cursor) {
        results {
            name
            owner {
                canonicalName
            }
            description
            visibility
            readme
            nonPublishing
            ...repositoryExport
        }
        cursor
    }
}

fragment repositoryExport on Repository {
    name
    owner {
        canonicalName
    }
    description
    visibility
    readme
    nonPublishing
}

query userWebhooks($cursor: Cursor) {
    userWebhooks(cursor: $cursor) {
        results {

M srht/listssrht/gql.go => srht/listssrht/gql.go +12 -1
@@ 727,8 727,19 @@ func MailingLists(client *gqlclient.Client, ctx context.Context, cursor *Cursor)
	return respData.Me, err
}

func ExportMailingList(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) {
	op := gqlclient.NewOperation("query exportMailingList ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\tlist(name: $name) {\n\t\t\t... mailingListExport\n\t\t}\n\t}\n}\nfragment mailingListExport on MailingList {\n\tname\n\tdescription\n\tvisibility\n\tpermitMime\n\trejectMime\n\tarchive\n}\n")
	op.Var("username", username)
	op.Var("name", name)
	var respData struct {
		User *User
	}
	err = client.Execute(ctx, op, &respData)
	return respData.User, err
}

func ExportMailingLists(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (me *User, err error) {
	op := gqlclient.NewOperation("query exportMailingLists ($cursor: Cursor) {\n\tme {\n\t\tlists(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\tname\n\t\t\t\tdescription\n\t\t\t\tvisibility\n\t\t\t\tpermitMime\n\t\t\t\trejectMime\n\t\t\t\tarchive\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\n")
	op := gqlclient.NewOperation("query exportMailingLists ($cursor: Cursor) {\n\tme {\n\t\tlists(cursor: $cursor) {\n\t\t\tresults {\n\t\t\t\t... mailingListExport\n\t\t\t}\n\t\t\tcursor\n\t\t}\n\t}\n}\nfragment mailingListExport on MailingList {\n\tname\n\tdescription\n\tvisibility\n\tpermitMime\n\trejectMime\n\tarchive\n}\n")
	op.Var("cursor", cursor)
	var respData struct {
		Me *User

M srht/listssrht/operations.graphql => srht/listssrht/operations.graphql +18 -6
@@ 12,22 12,34 @@ query mailingLists($cursor: Cursor) {
    }
}

query exportMailingList($username: String!, $name: String!) {
    user(username: $username) {
        list(name: $name) {
            ...mailingListExport
        }
    }
}

query exportMailingLists($cursor: Cursor) {
    me {
        lists(cursor: $cursor) {
            results {
                name
                description
                visibility
                permitMime
                rejectMime
                archive
                ...mailingListExport
            }
            cursor
        }
    }
}

fragment mailingListExport on MailingList {
    name
    description
    visibility
    permitMime
    rejectMime
    archive
}

query mailingListsByUser($username: String!, $cursor: Cursor) {
    user(username: $username) {
        lists(cursor: $cursor) {

M srht/pastesrht/gql.go => srht/pastesrht/gql.go +11 -1
@@ 343,7 343,7 @@ func Pastes(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (past
}

func PasteContents(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (pastes *PasteCursor, err error) {
	op := gqlclient.NewOperation("query pasteContents ($cursor: Cursor) {\n\tpastes(cursor: $cursor) {\n\t\tresults {\n\t\t\tid\n\t\t\tcreated\n\t\t\tvisibility\n\t\t\tfiles {\n\t\t\t\tfilename\n\t\t\t\tcontents\n\t\t\t}\n\t\t}\n\t\tcursor\n\t}\n}\n")
	op := gqlclient.NewOperation("query pasteContents ($cursor: Cursor) {\n\tpastes(cursor: $cursor) {\n\t\tresults {\n\t\t\t... pasteContents\n\t\t}\n\t\tcursor\n\t}\n}\nfragment pasteContents on Paste {\n\tid\n\tcreated\n\tvisibility\n\tfiles {\n\t\tfilename\n\t\tcontents\n\t}\n}\n")
	op.Var("cursor", cursor)
	var respData struct {
		Pastes *PasteCursor


@@ 352,6 352,16 @@ func PasteContents(client *gqlclient.Client, ctx context.Context, cursor *Cursor
	return respData.Pastes, err
}

func PasteContentsByID(client *gqlclient.Client, ctx context.Context, id string) (paste *Paste, err error) {
	op := gqlclient.NewOperation("query pasteContentsByID ($id: String!) {\n\tpaste(id: $id) {\n\t\t... pasteContents\n\t}\n}\nfragment pasteContents on Paste {\n\tid\n\tcreated\n\tvisibility\n\tfiles {\n\t\tfilename\n\t\tcontents\n\t}\n}\n")
	op.Var("id", id)
	var respData struct {
		Paste *Paste
	}
	err = client.Execute(ctx, op, &respData)
	return respData.Paste, err
}

func PasteCompletionList(client *gqlclient.Client, ctx context.Context) (pastes *PasteCursor, err error) {
	op := gqlclient.NewOperation("query pasteCompletionList {\n\tpastes {\n\t\tresults {\n\t\t\tid\n\t\t\tfiles {\n\t\t\t\tfilename\n\t\t\t}\n\t\t}\n\t}\n}\n")
	var respData struct {

M srht/pastesrht/operations.graphql => srht/pastesrht/operations.graphql +17 -7
@@ 48,18 48,28 @@ query pastes($cursor: Cursor) {
query pasteContents($cursor: Cursor) {
    pastes(cursor: $cursor) {
        results {
            id
            created
            visibility
            files {
                filename
                contents
            }
            ...pasteContents
        }
        cursor
    }
}

query pasteContentsByID($id: String!) {
    paste(id: $id) {
        ...pasteContents
    }
}

fragment pasteContents on Paste {
    id
    created
    visibility
    files {
        filename
        contents
    }
}

query pasteCompletionList {
    pastes {
        results {

M srht/todosrht/gql.go => srht/todosrht/gql.go +12 -1
@@ 937,8 937,19 @@ func TrackersByUser(client *gqlclient.Client, ctx context.Context, username stri
	return respData.User, err
}

func ExportTracker(client *gqlclient.Client, ctx context.Context, username string, name string) (user *User, err error) {
	op := gqlclient.NewOperation("query exportTracker ($username: String!, $name: String!) {\n\tuser(username: $username) {\n\t\ttracker(name: $name) {\n\t\t\t... trackerExport\n\t\t}\n\t}\n}\nfragment trackerExport on Tracker {\n\tname\n\tdescription\n\tvisibility\n\texport\n}\n")
	op.Var("username", username)
	op.Var("name", name)
	var respData struct {
		User *User
	}
	err = client.Execute(ctx, op, &respData)
	return respData.User, err
}

func ExportTrackers(client *gqlclient.Client, ctx context.Context, cursor *Cursor) (trackers *TrackerCursor, err error) {
	op := gqlclient.NewOperation("query exportTrackers ($cursor: Cursor) {\n\ttrackers(cursor: $cursor) {\n\t\tresults {\n\t\t\tname\n\t\t\tdescription\n\t\t\tvisibility\n\t\t\texport\n\t\t}\n\t\tcursor\n\t}\n}\n")
	op := gqlclient.NewOperation("query exportTrackers ($cursor: Cursor) {\n\ttrackers(cursor: $cursor) {\n\t\tresults {\n\t\t\t... trackerExport\n\t\t}\n\t\tcursor\n\t}\n}\nfragment trackerExport on Tracker {\n\tname\n\tdescription\n\tvisibility\n\texport\n}\n")
	op.Var("cursor", cursor)
	var respData struct {
		Trackers *TrackerCursor

M srht/todosrht/operations.graphql => srht/todosrht/operations.graphql +16 -4
@@ 21,18 21,30 @@ fragment trackers on TrackerCursor {
    cursor
}

query exportTracker($username: String!, $name: String!) {
    user(username: $username) {
        tracker(name: $name) {
            ...trackerExport
        }
    }
}

query exportTrackers($cursor: Cursor) {
    trackers(cursor: $cursor) {
        results {
            name
            description
            visibility
            export
            ...trackerExport
        }
        cursor
    }
}

fragment trackerExport on Tracker {
    name
    description
    visibility
    export
}

query trackerIDByName($name: String!) {
    me {
        tracker(name: $name) {