Merge pull request #171 from MichaelMure/graphql-mutation-relay

Michael Muré created

Graphql mutation relay

Change summary

bridge/github/import.go          |   4 
bridge/launchpad/import.go       |   2 
cache/multi_repo_cache.go        |   4 
cache/repo_cache.go              |  18 
cache/repo_cache_test.go         |   8 
commands/add.go                  |   2 
commands/select/select_test.go   |   6 
graphql/gqlgen.yml               |   4 
graphql/graph/gen_graph.go       | 708 ++++++++++++++++++++++++---------
graphql/models/gen_models.go     | 204 +++++++++
graphql/resolvers/label.go       |  23 +
graphql/resolvers/mutation.go    | 131 ++++-
graphql/resolvers/operations.go  |   2 
graphql/resolvers/query.go       |   4 
graphql/resolvers/root.go        |  12 
graphql/schema/mutations.graphql | 170 ++++++++
graphql/schema/root.graphql      |  29 
termui/termui.go                 |   2 
18 files changed, 1,062 insertions(+), 271 deletions(-)

Detailed changes

bridge/github/import.go 🔗

@@ -106,7 +106,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
 			}
 
 			// create bug
-			b, err = repo.NewBugRaw(
+			b, _, err = repo.NewBugRaw(
 				author,
 				issue.CreatedAt.Unix(),
 				issue.Title,
@@ -140,7 +140,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline
 			// if the bug doesn't exist
 			if b == nil {
 				// we create the bug as soon as we have a legit first edition
-				b, err = repo.NewBugRaw(
+				b, _, err = repo.NewBugRaw(
 					author,
 					issue.CreatedAt.Unix(),
 					issue.Title,

bridge/launchpad/import.go 🔗

@@ -74,7 +74,7 @@ func (li *launchpadImporter) ImportAll(repo *cache.RepoCache, since time.Time) e
 
 		if err == bug.ErrBugNotExist {
 			createdAt, _ := time.Parse(time.RFC3339, lpBug.CreatedAt)
-			b, err = repo.NewBugRaw(
+			b, _, err = repo.NewBugRaw(
 				owner,
 				createdAt.Unix(),
 				lpBug.Title,

cache/multi_repo_cache.go 🔗

@@ -41,7 +41,7 @@ func (c *MultiRepoCache) RegisterDefaultRepository(repo repository.ClockedRepo)
 	return nil
 }
 
-// ResolveRepo retrieve a repository by name
+// DefaultRepo retrieve the default repository
 func (c *MultiRepoCache) DefaultRepo() (*RepoCache, error) {
 	if len(c.repos) != 1 {
 		return nil, fmt.Errorf("repository is not unique")
@@ -54,7 +54,7 @@ func (c *MultiRepoCache) DefaultRepo() (*RepoCache, error) {
 	panic("unreachable")
 }
 
-// DefaultRepo retrieve the default repository
+// ResolveRepo retrieve a repository with a reference
 func (c *MultiRepoCache) ResolveRepo(ref string) (*RepoCache, error) {
 	r, ok := c.repos[ref]
 	if !ok {

cache/repo_cache.go 🔗

@@ -563,16 +563,16 @@ func (c *RepoCache) ValidLabels() []bug.Label {
 
 // NewBug create a new bug
 // The new bug is written in the repository (commit)
-func (c *RepoCache) NewBug(title string, message string) (*BugCache, error) {
+func (c *RepoCache) NewBug(title string, message string) (*BugCache, *bug.CreateOperation, error) {
 	return c.NewBugWithFiles(title, message, nil)
 }
 
 // NewBugWithFiles create a new bug with attached files for the message
 // The new bug is written in the repository (commit)
-func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Hash) (*BugCache, error) {
+func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Hash) (*BugCache, *bug.CreateOperation, error) {
 	author, err := c.GetUserIdentity()
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
 	return c.NewBugRaw(author, time.Now().Unix(), title, message, files, nil)
@@ -581,10 +581,10 @@ func (c *RepoCache) NewBugWithFiles(title string, message string, files []git.Ha
 // NewBugWithFilesMeta create a new bug with attached files for the message, as
 // well as metadata for the Create operation.
 // The new bug is written in the repository (commit)
-func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, error) {
+func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title string, message string, files []git.Hash, metadata map[string]string) (*BugCache, *bug.CreateOperation, error) {
 	b, op, err := bug.CreateWithFiles(author.Identity, unixTime, title, message, files)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
 	for key, value := range metadata {
@@ -593,11 +593,11 @@ func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title strin
 
 	err = b.Commit(c.repo)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
 	if _, has := c.bugs[b.Id()]; has {
-		return nil, fmt.Errorf("bug %s already exist in the cache", b.Id())
+		return nil, nil, fmt.Errorf("bug %s already exist in the cache", b.Id())
 	}
 
 	cached := NewBugCache(c, b)
@@ -606,10 +606,10 @@ func (c *RepoCache) NewBugRaw(author *IdentityCache, unixTime int64, title strin
 	// force the write of the excerpt
 	err = c.bugUpdated(b.Id())
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
-	return cached, nil
+	return cached, op, nil
 }
 
 // Fetch retrieve updates from a remote

cache/repo_cache_test.go 🔗

@@ -37,11 +37,11 @@ func TestCache(t *testing.T) {
 	require.Len(t, cache.identities, 2)
 
 	// Create a bug
-	bug1, err := cache.NewBug("title", "message")
+	bug1, _, err := cache.NewBug("title", "message")
 	require.NoError(t, err)
 
 	// It's possible to create two identical bugs
-	bug2, err := cache.NewBug("title", "message")
+	bug2, _, err := cache.NewBug("title", "message")
 	require.NoError(t, err)
 
 	// two identical bugs yield a different id
@@ -125,7 +125,7 @@ func TestPushPull(t *testing.T) {
 	require.NoError(t, err)
 
 	// Create a bug in A
-	_, err = cacheA.NewBug("bug1", "message")
+	_, _, err = cacheA.NewBug("bug1", "message")
 	require.NoError(t, err)
 
 	// A --> remote --> B
@@ -145,7 +145,7 @@ func TestPushPull(t *testing.T) {
 	require.NoError(t, err)
 
 	// B --> remote --> A
-	_, err = cacheB.NewBug("bug2", "message")
+	_, _, err = cacheB.NewBug("bug2", "message")
 	require.NoError(t, err)
 
 	_, err = cacheB.Push("origin")

commands/add.go 🔗

@@ -44,7 +44,7 @@ func runAddBug(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	b, err := backend.NewBug(addTitle, addMessage)
+	b, _, err := backend.NewBug(addTitle, addMessage)
 	if err != nil {
 		return err
 	}

commands/select/select_test.go 🔗

@@ -33,14 +33,14 @@ func TestSelect(t *testing.T) {
 	require.NoError(t, err)
 
 	for i := 0; i < 10; i++ {
-		_, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+		_, _, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
 		require.NoError(t, err)
 	}
 
 	// and two more for testing
-	b1, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+	b1, _, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
 	require.NoError(t, err)
-	b2, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
+	b2, _, err := repoCache.NewBugRaw(rene, time.Now().Unix(), "title", "message", nil, nil)
 	require.NoError(t, err)
 
 	err = Select(repoCache, b1.Id())

graphql/gqlgen.yml 🔗

@@ -48,4 +48,6 @@ models:
   SetStatusTimelineItem:
     model: github.com/MichaelMure/git-bug/bug.SetStatusTimelineItem
   SetTitleTimelineItem:
-    model: github.com/MichaelMure/git-bug/bug.SetTitleTimelineItem
+    model: github.com/MichaelMure/git-bug/bug.SetTitleTimelineItem
+  LabelChangeResult:
+    model: github.com/MichaelMure/git-bug/bug.LabelChangeResult

graphql/graph/gen_graph.go 🔗

@@ -52,6 +52,7 @@ type ResolverRoot interface {
 	Identity() IdentityResolver
 	Label() LabelResolver
 	LabelChangeOperation() LabelChangeOperationResolver
+	LabelChangeResult() LabelChangeResultResolver
 	LabelChangeTimelineItem() LabelChangeTimelineItemResolver
 	Mutation() MutationResolver
 	Query() QueryResolver
@@ -74,6 +75,12 @@ type ComplexityRoot struct {
 		Message func(childComplexity int) int
 	}
 
+	AddCommentPayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+		Operation        func(childComplexity int) int
+	}
+
 	AddCommentTimelineItem struct {
 		Author         func(childComplexity int) int
 		CreatedAt      func(childComplexity int) int
@@ -114,6 +121,19 @@ type ComplexityRoot struct {
 		Node   func(childComplexity int) int
 	}
 
+	ChangeLabelPayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+		Operation        func(childComplexity int) int
+		Results          func(childComplexity int) int
+	}
+
+	CloseBugPayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+		Operation        func(childComplexity int) int
+	}
+
 	Color struct {
 		B func(childComplexity int) int
 		G func(childComplexity int) int
@@ -143,6 +163,16 @@ type ComplexityRoot struct {
 		Message func(childComplexity int) int
 	}
 
+	CommitAsNeededPayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+	}
+
+	CommitPayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+	}
+
 	CreateOperation struct {
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
@@ -209,6 +239,11 @@ type ComplexityRoot struct {
 		Removed func(childComplexity int) int
 	}
 
+	LabelChangeResult struct {
+		Label  func(childComplexity int) int
+		Status func(childComplexity int) int
+	}
+
 	LabelChangeTimelineItem struct {
 		Added   func(childComplexity int) int
 		Author  func(childComplexity int) int
@@ -218,13 +253,26 @@ type ComplexityRoot struct {
 	}
 
 	Mutation struct {
-		AddComment   func(childComplexity int, repoRef *string, prefix string, message string, files []git.Hash) int
-		ChangeLabels func(childComplexity int, repoRef *string, prefix string, added []string, removed []string) int
-		Close        func(childComplexity int, repoRef *string, prefix string) int
-		Commit       func(childComplexity int, repoRef *string, prefix string) int
-		NewBug       func(childComplexity int, repoRef *string, title string, message string, files []git.Hash) int
-		Open         func(childComplexity int, repoRef *string, prefix string) int
-		SetTitle     func(childComplexity int, repoRef *string, prefix string, title string) int
+		AddComment     func(childComplexity int, input models.AddCommentInput) int
+		ChangeLabels   func(childComplexity int, input *models.ChangeLabelInput) int
+		CloseBug       func(childComplexity int, input models.CloseBugInput) int
+		Commit         func(childComplexity int, input models.CommitInput) int
+		CommitAsNeeded func(childComplexity int, input models.CommitAsNeededInput) int
+		NewBug         func(childComplexity int, input models.NewBugInput) int
+		OpenBug        func(childComplexity int, input models.OpenBugInput) int
+		SetTitle       func(childComplexity int, input models.SetTitleInput) int
+	}
+
+	NewBugPayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+		Operation        func(childComplexity int) int
+	}
+
+	OpenBugPayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+		Operation        func(childComplexity int) int
 	}
 
 	OperationConnection struct {
@@ -248,7 +296,7 @@ type ComplexityRoot struct {
 
 	Query struct {
 		DefaultRepository func(childComplexity int) int
-		Repository        func(childComplexity int, id string) int
+		Repository        func(childComplexity int, ref string) int
 	}
 
 	Repository struct {
@@ -282,6 +330,12 @@ type ComplexityRoot struct {
 		Was    func(childComplexity int) int
 	}
 
+	SetTitlePayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+		Operation        func(childComplexity int) int
+	}
+
 	SetTitleTimelineItem struct {
 		Author func(childComplexity int) int
 		Date   func(childComplexity int) int
@@ -355,21 +409,25 @@ type LabelResolver interface {
 type LabelChangeOperationResolver interface {
 	Date(ctx context.Context, obj *bug.LabelChangeOperation) (*time.Time, error)
 }
+type LabelChangeResultResolver interface {
+	Status(ctx context.Context, obj *bug.LabelChangeResult) (models.LabelChangeStatus, error)
+}
 type LabelChangeTimelineItemResolver interface {
 	Date(ctx context.Context, obj *bug.LabelChangeTimelineItem) (*time.Time, error)
 }
 type MutationResolver interface {
-	NewBug(ctx context.Context, repoRef *string, title string, message string, files []git.Hash) (*bug.Snapshot, error)
-	AddComment(ctx context.Context, repoRef *string, prefix string, message string, files []git.Hash) (*bug.Snapshot, error)
-	ChangeLabels(ctx context.Context, repoRef *string, prefix string, added []string, removed []string) (*bug.Snapshot, error)
-	Open(ctx context.Context, repoRef *string, prefix string) (*bug.Snapshot, error)
-	Close(ctx context.Context, repoRef *string, prefix string) (*bug.Snapshot, error)
-	SetTitle(ctx context.Context, repoRef *string, prefix string, title string) (*bug.Snapshot, error)
-	Commit(ctx context.Context, repoRef *string, prefix string) (*bug.Snapshot, error)
+	NewBug(ctx context.Context, input models.NewBugInput) (*models.NewBugPayload, error)
+	AddComment(ctx context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error)
+	ChangeLabels(ctx context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error)
+	OpenBug(ctx context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error)
+	CloseBug(ctx context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error)
+	SetTitle(ctx context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error)
+	Commit(ctx context.Context, input models.CommitInput) (*models.CommitPayload, error)
+	CommitAsNeeded(ctx context.Context, input models.CommitAsNeededInput) (*models.CommitAsNeededPayload, error)
 }
 type QueryResolver interface {
 	DefaultRepository(ctx context.Context) (*models.Repository, error)
-	Repository(ctx context.Context, id string) (*models.Repository, error)
+	Repository(ctx context.Context, ref string) (*models.Repository, error)
 }
 type RepositoryResolver interface {
 	AllBugs(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, query *string) (*models.BugConnection, error)
@@ -444,6 +502,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.AddCommentOperation.Message(childComplexity), true
 
+	case "AddCommentPayload.bug":
+		if e.complexity.AddCommentPayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.AddCommentPayload.Bug(childComplexity), true
+
+	case "AddCommentPayload.clientMutationId":
+		if e.complexity.AddCommentPayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.AddCommentPayload.ClientMutationID(childComplexity), true
+
+	case "AddCommentPayload.operation":
+		if e.complexity.AddCommentPayload.Operation == nil {
+			break
+		}
+
+		return e.complexity.AddCommentPayload.Operation(childComplexity), true
+
 	case "AddCommentTimelineItem.author":
 		if e.complexity.AddCommentTimelineItem.Author == nil {
 			break
@@ -665,6 +744,55 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.BugEdge.Node(childComplexity), true
 
+	case "ChangeLabelPayload.bug":
+		if e.complexity.ChangeLabelPayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.ChangeLabelPayload.Bug(childComplexity), true
+
+	case "ChangeLabelPayload.clientMutationId":
+		if e.complexity.ChangeLabelPayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.ChangeLabelPayload.ClientMutationID(childComplexity), true
+
+	case "ChangeLabelPayload.operation":
+		if e.complexity.ChangeLabelPayload.Operation == nil {
+			break
+		}
+
+		return e.complexity.ChangeLabelPayload.Operation(childComplexity), true
+
+	case "ChangeLabelPayload.results":
+		if e.complexity.ChangeLabelPayload.Results == nil {
+			break
+		}
+
+		return e.complexity.ChangeLabelPayload.Results(childComplexity), true
+
+	case "CloseBugPayload.bug":
+		if e.complexity.CloseBugPayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.CloseBugPayload.Bug(childComplexity), true
+
+	case "CloseBugPayload.clientMutationId":
+		if e.complexity.CloseBugPayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.CloseBugPayload.ClientMutationID(childComplexity), true
+
+	case "CloseBugPayload.operation":
+		if e.complexity.CloseBugPayload.Operation == nil {
+			break
+		}
+
+		return e.complexity.CloseBugPayload.Operation(childComplexity), true
+
 	case "Color.B":
 		if e.complexity.Color.B == nil {
 			break
@@ -763,6 +891,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.CommentHistoryStep.Message(childComplexity), true
 
+	case "CommitAsNeededPayload.bug":
+		if e.complexity.CommitAsNeededPayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.CommitAsNeededPayload.Bug(childComplexity), true
+
+	case "CommitAsNeededPayload.clientMutationId":
+		if e.complexity.CommitAsNeededPayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.CommitAsNeededPayload.ClientMutationID(childComplexity), true
+
+	case "CommitPayload.bug":
+		if e.complexity.CommitPayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.CommitPayload.Bug(childComplexity), true
+
+	case "CommitPayload.clientMutationId":
+		if e.complexity.CommitPayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.CommitPayload.ClientMutationID(childComplexity), true
+
 	case "CreateOperation.author":
 		if e.complexity.CreateOperation.Author == nil {
 			break
@@ -1057,6 +1213,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.LabelChangeOperation.Removed(childComplexity), true
 
+	case "LabelChangeResult.label":
+		if e.complexity.LabelChangeResult.Label == nil {
+			break
+		}
+
+		return e.complexity.LabelChangeResult.Label(childComplexity), true
+
+	case "LabelChangeResult.status":
+		if e.complexity.LabelChangeResult.Status == nil {
+			break
+		}
+
+		return e.complexity.LabelChangeResult.Status(childComplexity), true
+
 	case "LabelChangeTimelineItem.added":
 		if e.complexity.LabelChangeTimelineItem.Added == nil {
 			break
@@ -1102,7 +1272,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 			return 0, false
 		}
 
-		return e.complexity.Mutation.AddComment(childComplexity, args["repoRef"].(*string), args["prefix"].(string), args["message"].(string), args["files"].([]git.Hash)), true
+		return e.complexity.Mutation.AddComment(childComplexity, args["input"].(models.AddCommentInput)), true
 
 	case "Mutation.changeLabels":
 		if e.complexity.Mutation.ChangeLabels == nil {
@@ -1114,19 +1284,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 			return 0, false
 		}
 
-		return e.complexity.Mutation.ChangeLabels(childComplexity, args["repoRef"].(*string), args["prefix"].(string), args["added"].([]string), args["removed"].([]string)), true
+		return e.complexity.Mutation.ChangeLabels(childComplexity, args["input"].(*models.ChangeLabelInput)), true
 
-	case "Mutation.close":
-		if e.complexity.Mutation.Close == nil {
+	case "Mutation.closeBug":
+		if e.complexity.Mutation.CloseBug == nil {
 			break
 		}
 
-		args, err := ec.field_Mutation_close_args(context.TODO(), rawArgs)
+		args, err := ec.field_Mutation_closeBug_args(context.TODO(), rawArgs)
 		if err != nil {
 			return 0, false
 		}
 
-		return e.complexity.Mutation.Close(childComplexity, args["repoRef"].(*string), args["prefix"].(string)), true
+		return e.complexity.Mutation.CloseBug(childComplexity, args["input"].(models.CloseBugInput)), true
 
 	case "Mutation.commit":
 		if e.complexity.Mutation.Commit == nil {
@@ -1138,7 +1308,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 			return 0, false
 		}
 
-		return e.complexity.Mutation.Commit(childComplexity, args["repoRef"].(*string), args["prefix"].(string)), true
+		return e.complexity.Mutation.Commit(childComplexity, args["input"].(models.CommitInput)), true
+
+	case "Mutation.commitAsNeeded":
+		if e.complexity.Mutation.CommitAsNeeded == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_commitAsNeeded_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.CommitAsNeeded(childComplexity, args["input"].(models.CommitAsNeededInput)), true
 
 	case "Mutation.newBug":
 		if e.complexity.Mutation.NewBug == nil {
@@ -1150,19 +1332,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 			return 0, false
 		}
 
-		return e.complexity.Mutation.NewBug(childComplexity, args["repoRef"].(*string), args["title"].(string), args["message"].(string), args["files"].([]git.Hash)), true
+		return e.complexity.Mutation.NewBug(childComplexity, args["input"].(models.NewBugInput)), true
 
-	case "Mutation.open":
-		if e.complexity.Mutation.Open == nil {
+	case "Mutation.openBug":
+		if e.complexity.Mutation.OpenBug == nil {
 			break
 		}
 
-		args, err := ec.field_Mutation_open_args(context.TODO(), rawArgs)
+		args, err := ec.field_Mutation_openBug_args(context.TODO(), rawArgs)
 		if err != nil {
 			return 0, false
 		}
 
-		return e.complexity.Mutation.Open(childComplexity, args["repoRef"].(*string), args["prefix"].(string)), true
+		return e.complexity.Mutation.OpenBug(childComplexity, args["input"].(models.OpenBugInput)), true
 
 	case "Mutation.setTitle":
 		if e.complexity.Mutation.SetTitle == nil {
@@ -1174,7 +1356,49 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 			return 0, false
 		}
 
-		return e.complexity.Mutation.SetTitle(childComplexity, args["repoRef"].(*string), args["prefix"].(string), args["title"].(string)), true
+		return e.complexity.Mutation.SetTitle(childComplexity, args["input"].(models.SetTitleInput)), true
+
+	case "NewBugPayload.bug":
+		if e.complexity.NewBugPayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.NewBugPayload.Bug(childComplexity), true
+
+	case "NewBugPayload.clientMutationId":
+		if e.complexity.NewBugPayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.NewBugPayload.ClientMutationID(childComplexity), true
+
+	case "NewBugPayload.operation":
+		if e.complexity.NewBugPayload.Operation == nil {
+			break
+		}
+
+		return e.complexity.NewBugPayload.Operation(childComplexity), true
+
+	case "OpenBugPayload.bug":
+		if e.complexity.OpenBugPayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.OpenBugPayload.Bug(childComplexity), true
+
+	case "OpenBugPayload.clientMutationId":
+		if e.complexity.OpenBugPayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.OpenBugPayload.ClientMutationID(childComplexity), true
+
+	case "OpenBugPayload.operation":
+		if e.complexity.OpenBugPayload.Operation == nil {
+			break
+		}
+
+		return e.complexity.OpenBugPayload.Operation(childComplexity), true
 
 	case "OperationConnection.edges":
 		if e.complexity.OperationConnection.Edges == nil {
@@ -1263,7 +1487,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 			return 0, false
 		}
 
-		return e.complexity.Query.Repository(childComplexity, args["id"].(string)), true
+		return e.complexity.Query.Repository(childComplexity, args["ref"].(string)), true
 
 	case "Repository.allBugs":
 		if e.complexity.Repository.AllBugs == nil {
@@ -1418,6 +1642,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.SetTitleOperation.Was(childComplexity), true
 
+	case "SetTitlePayload.bug":
+		if e.complexity.SetTitlePayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.SetTitlePayload.Bug(childComplexity), true
+
+	case "SetTitlePayload.clientMutationId":
+		if e.complexity.SetTitlePayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.SetTitlePayload.ClientMutationID(childComplexity), true
+
+	case "SetTitlePayload.operation":
+		if e.complexity.SetTitlePayload.Operation == nil {
+			break
+		}
+
+		return e.complexity.SetTitlePayload.Operation(childComplexity), true
+
 	case "SetTitleTimelineItem.author":
 		if e.complexity.SetTitleTimelineItem.Author == nil {
 			break
@@ -1723,6 +1968,177 @@ type IdentityEdge {
     cursor: String!
     node: Identity!
 }`},
+	&ast.Source{Name: "schema/mutations.graphql", Input: `input NewBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The title of the new bug."""
+    title: String!
+    """The first message of the new bug."""
+    message: String!
+    """The collection of file's hash required for the first message."""
+    files: [Hash!]
+}
+
+type NewBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The created bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: CreateOperation!
+}
+
+input AddCommentInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The first message of the new bug."""
+    message: String!
+    """The collection of file's hash required for the first message."""
+    files: [Hash!]
+}
+
+type AddCommentPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: AddCommentOperation!
+}
+
+input ChangeLabelInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The list of label to add."""
+    added: [String!]
+    """The list of label to remove."""
+    Removed: [String!]
+}
+
+enum LabelChangeStatus {
+    ADDED
+    REMOVED
+    DUPLICATE_IN_OP
+    ALREADY_EXIST
+    DOESNT_EXIST
+}
+
+type LabelChangeResult {
+    """The source label."""
+    label: Label!
+    """The effect this label had."""
+    status: LabelChangeStatus!
+}
+
+type ChangeLabelPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: LabelChangeOperation!
+    """The effect each source label had."""
+    results: [LabelChangeResult]!
+}
+
+input OpenBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+}
+
+type OpenBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: SetStatusOperation!
+}
+
+input CloseBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+}
+
+type CloseBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: SetStatusOperation!
+}
+
+input SetTitleInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The new title."""
+    title: String!
+}
+
+type SetTitlePayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation"""
+    operation: SetTitleOperation!
+}
+
+input CommitInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+}
+
+type CommitPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+}
+
+input CommitAsNeededInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+}
+
+type CommitAsNeededPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+}
+`},
 	&ast.Source{Name: "schema/operations.graphql", Input: `"""An operation applied to a bug."""
 interface Operation {
     """The hash of the operation"""
@@ -1863,20 +2279,29 @@ type Repository {
     validLabels: [Label!]!
 }`},
 	&ast.Source{Name: "schema/root.graphql", Input: `type Query {
+    """The default unnamend repository."""
     defaultRepository: Repository
-    repository(id: String!): Repository
+    """Access a repository by reference/name."""
+    repository(ref: String!): Repository
 }
 
 type Mutation {
-    newBug(repoRef: String, title: String!, message: String!, files: [Hash!]): Bug!
-
-    addComment(repoRef: String, prefix: String!, message: String!, files: [Hash!]): Bug!
-    changeLabels(repoRef: String, prefix: String!, added: [String!], removed: [String!]): Bug!
-    open(repoRef: String, prefix: String!): Bug!
-    close(repoRef: String, prefix: String!): Bug!
-    setTitle(repoRef: String, prefix: String!, title: String!): Bug!
-
-    commit(repoRef: String, prefix: String!): Bug!
+    """Create a new bug"""
+    newBug(input: NewBugInput!): NewBugPayload!
+    """Add a new comment to a bug"""
+    addComment(input: AddCommentInput!): AddCommentPayload!
+    """Add or remove a set of label on a bug"""
+    changeLabels(input: ChangeLabelInput): ChangeLabelPayload!
+    """Change a bug's status to open"""
+    openBug(input: OpenBugInput!): OpenBugPayload!
+    """Change a bug's status to closed"""
+    closeBug(input: CloseBugInput!): CloseBugPayload!
+    """Change a bug's titlel"""
+    setTitle(input: SetTitleInput!): SetTitlePayload!
+    """Commit write the pending operations into storage. This mutation fail if nothing is pending"""
+    commit(input: CommitInput!): CommitPayload!
+    """Commit write the pending operations into storage. This mutation succed if nothing is pending"""
+    commitAsNeeded(input: CommitAsNeededInput!): CommitAsNeededPayload!
 }
 `},
 	&ast.Source{Name: "schema/timeline.graphql", Input: `"""An item in the timeline of events"""
@@ -2204,210 +2629,112 @@ func (ec *executionContext) field_Bug_timeline_args(ctx context.Context, rawArgs
 func (ec *executionContext) field_Mutation_addComment_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
-	var arg0 *string
-	if tmp, ok := rawArgs["repoRef"]; ok {
-		arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["repoRef"] = arg0
-	var arg1 string
-	if tmp, ok := rawArgs["prefix"]; ok {
-		arg1, err = ec.unmarshalNString2string(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["prefix"] = arg1
-	var arg2 string
-	if tmp, ok := rawArgs["message"]; ok {
-		arg2, err = ec.unmarshalNString2string(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["message"] = arg2
-	var arg3 []git.Hash
-	if tmp, ok := rawArgs["files"]; ok {
-		arg3, err = ec.unmarshalOHash2ᚕgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋutilᚋgitᚐHash(ctx, tmp)
+	var arg0 models.AddCommentInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalNAddCommentInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐAddCommentInput(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["files"] = arg3
+	args["input"] = arg0
 	return args, nil
 }
 
 func (ec *executionContext) field_Mutation_changeLabels_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
-	var arg0 *string
-	if tmp, ok := rawArgs["repoRef"]; ok {
-		arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["repoRef"] = arg0
-	var arg1 string
-	if tmp, ok := rawArgs["prefix"]; ok {
-		arg1, err = ec.unmarshalNString2string(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["prefix"] = arg1
-	var arg2 []string
-	if tmp, ok := rawArgs["added"]; ok {
-		arg2, err = ec.unmarshalOString2ᚕstring(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["added"] = arg2
-	var arg3 []string
-	if tmp, ok := rawArgs["removed"]; ok {
-		arg3, err = ec.unmarshalOString2ᚕstring(ctx, tmp)
+	var arg0 *models.ChangeLabelInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalOChangeLabelInput2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐChangeLabelInput(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["removed"] = arg3
+	args["input"] = arg0
 	return args, nil
 }
 
-func (ec *executionContext) field_Mutation_close_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+func (ec *executionContext) field_Mutation_closeBug_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
-	var arg0 *string
-	if tmp, ok := rawArgs["repoRef"]; ok {
-		arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
+	var arg0 models.CloseBugInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalNCloseBugInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCloseBugInput(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["repoRef"] = arg0
-	var arg1 string
-	if tmp, ok := rawArgs["prefix"]; ok {
-		arg1, err = ec.unmarshalNString2string(ctx, tmp)
+	args["input"] = arg0
+	return args, nil
+}
+
+func (ec *executionContext) field_Mutation_commitAsNeeded_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 models.CommitAsNeededInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalNCommitAsNeededInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededInput(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["prefix"] = arg1
+	args["input"] = arg0
 	return args, nil
 }
 
 func (ec *executionContext) field_Mutation_commit_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
-	var arg0 *string
-	if tmp, ok := rawArgs["repoRef"]; ok {
-		arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["repoRef"] = arg0
-	var arg1 string
-	if tmp, ok := rawArgs["prefix"]; ok {
-		arg1, err = ec.unmarshalNString2string(ctx, tmp)
+	var arg0 models.CommitInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalNCommitInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitInput(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["prefix"] = arg1
+	args["input"] = arg0
 	return args, nil
 }
 
 func (ec *executionContext) field_Mutation_newBug_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
-	var arg0 *string
-	if tmp, ok := rawArgs["repoRef"]; ok {
-		arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["repoRef"] = arg0
-	var arg1 string
-	if tmp, ok := rawArgs["title"]; ok {
-		arg1, err = ec.unmarshalNString2string(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["title"] = arg1
-	var arg2 string
-	if tmp, ok := rawArgs["message"]; ok {
-		arg2, err = ec.unmarshalNString2string(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["message"] = arg2
-	var arg3 []git.Hash
-	if tmp, ok := rawArgs["files"]; ok {
-		arg3, err = ec.unmarshalOHash2ᚕgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋutilᚋgitᚐHash(ctx, tmp)
+	var arg0 models.NewBugInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalNNewBugInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐNewBugInput(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["files"] = arg3
+	args["input"] = arg0
 	return args, nil
 }
 
-func (ec *executionContext) field_Mutation_open_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+func (ec *executionContext) field_Mutation_openBug_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
-	var arg0 *string
-	if tmp, ok := rawArgs["repoRef"]; ok {
-		arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["repoRef"] = arg0
-	var arg1 string
-	if tmp, ok := rawArgs["prefix"]; ok {
-		arg1, err = ec.unmarshalNString2string(ctx, tmp)
+	var arg0 models.OpenBugInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalNOpenBugInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐOpenBugInput(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["prefix"] = arg1
+	args["input"] = arg0
 	return args, nil
 }
 
 func (ec *executionContext) field_Mutation_setTitle_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
-	var arg0 *string
-	if tmp, ok := rawArgs["repoRef"]; ok {
-		arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["repoRef"] = arg0
-	var arg1 string
-	if tmp, ok := rawArgs["prefix"]; ok {
-		arg1, err = ec.unmarshalNString2string(ctx, tmp)
-		if err != nil {
-			return nil, err
-		}
-	}
-	args["prefix"] = arg1
-	var arg2 string
-	if tmp, ok := rawArgs["title"]; ok {
-		arg2, err = ec.unmarshalNString2string(ctx, tmp)
+	var arg0 models.SetTitleInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalNSetTitleInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐSetTitleInput(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["title"] = arg2
+	args["input"] = arg0
 	return args, nil
 }
 
@@ -2429,13 +2756,13 @@ func (ec *executionContext) field_Query_repository_args(ctx context.Context, raw
 	var err error
 	args := map[string]interface{}{}
 	var arg0 string
-	if tmp, ok := rawArgs["id"]; ok {
+	if tmp, ok := rawArgs["ref"]; ok {
 		arg0, err = ec.unmarshalNString2string(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
 	}
-	args["id"] = arg0
+	args["ref"] = arg0
 	return args, nil
 }
 
@@ -2718,38 +3045,35 @@ func (ec *executionContext) _AddCommentOperation_files(ctx context.Context, fiel
 	return ec.marshalNHash2ᚕgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋutilᚋgitᚐHash(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) _AddCommentTimelineItem_hash(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
+func (ec *executionContext) _AddCommentPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.AddCommentPayload) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
 	rctx := &graphql.ResolverContext{
-		Object:   "AddCommentTimelineItem",
+		Object:   "AddCommentPayload",
 		Field:    field,
 		Args:     nil,
-		IsMethod: true,
+		IsMethod: false,
 	}
 	ctx = graphql.WithResolverContext(ctx, rctx)
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.Hash(), nil
+		return obj.ClientMutationID, nil
 	})
 	if resTmp == nil {
-		if !ec.HasError(rctx) {
-			ec.Errorf(ctx, "must not be null")
-		}
 		return graphql.Null
 	}
-	res := resTmp.(git.Hash)
+	res := resTmp.(*string)
 	rctx.Result = res
 	ctx = ec.Tracer.StartFieldChildExecution(ctx)
-	return ec.marshalNHash2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋutilᚋgitᚐHash(ctx, field.Selections, res)
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) _AddCommentTimelineItem_author(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentTimelineItem) graphql.Marshaler {
+func (ec *executionContext) _AddCommentPayload_bug(ctx context.Context, field graphql.CollectedField, obj *models.AddCommentPayload) graphql.Marshaler {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() { ec.Tracer.EndFieldExecution(ctx) }()
 	rctx := &graphql.ResolverContext{
-		Object:   "AddCommentTimelineItem",
+		Object:   "AddCommentPayload",
 		Field:    field,
 		Args:     nil,
 		IsMethod: false,
@@ -2758,7 +3082,7 @@ func (ec *executionContext) _AddCommentTimelineItem_author(ctx context.Context,
 	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
 	resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
 		ctx = rctx // use context from middleware stack in children
-		return obj.Author, nil
+		return obj.Bug, nil
 	})
 	if resTmp == nil {
 		if !ec.HasError(rctx) {

graphql/models/gen_models.go 🔗

@@ -9,6 +9,7 @@ import (
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/git"
 )
 
 // An object that has an author.
@@ -16,6 +17,28 @@ type Authored interface {
 	IsAuthored()
 }
 
+type AddCommentInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// "The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The bug ID's prefix.
+	Prefix string `json:"prefix"`
+	// The first message of the new bug.
+	Message string `json:"message"`
+	// The collection of file's hash required for the first message.
+	Files []git.Hash `json:"files"`
+}
+
+type AddCommentPayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The affected bug.
+	Bug *bug.Snapshot `json:"bug"`
+	// The resulting operation.
+	Operation *bug.AddCommentOperation `json:"operation"`
+}
+
 // The connection type for Bug.
 type BugConnection struct {
 	// A list of edges.
@@ -35,6 +58,48 @@ type BugEdge struct {
 	Node *bug.Snapshot `json:"node"`
 }
 
+type ChangeLabelInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// "The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The bug ID's prefix.
+	Prefix string `json:"prefix"`
+	// The list of label to add.
+	Added []string `json:"added"`
+	// The list of label to remove.
+	Removed []string `json:"Removed"`
+}
+
+type ChangeLabelPayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The affected bug.
+	Bug *bug.Snapshot `json:"bug"`
+	// The resulting operation.
+	Operation *bug.LabelChangeOperation `json:"operation"`
+	// The effect each source label had.
+	Results []*bug.LabelChangeResult `json:"results"`
+}
+
+type CloseBugInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// "The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The bug ID's prefix.
+	Prefix string `json:"prefix"`
+}
+
+type CloseBugPayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The affected bug.
+	Bug *bug.Snapshot `json:"bug"`
+	// The resulting operation.
+	Operation *bug.SetStatusOperation `json:"operation"`
+}
+
 type CommentConnection struct {
 	Edges      []*CommentEdge `json:"edges"`
 	Nodes      []*bug.Comment `json:"nodes"`
@@ -47,6 +112,38 @@ type CommentEdge struct {
 	Node   *bug.Comment `json:"node"`
 }
 
+type CommitAsNeededInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// "The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The bug ID's prefix.
+	Prefix string `json:"prefix"`
+}
+
+type CommitAsNeededPayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The affected bug.
+	Bug *bug.Snapshot `json:"bug"`
+}
+
+type CommitInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// "The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The bug ID's prefix.
+	Prefix string `json:"prefix"`
+}
+
+type CommitPayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The affected bug.
+	Bug *bug.Snapshot `json:"bug"`
+}
+
 type IdentityConnection struct {
 	Edges      []*IdentityEdge      `json:"edges"`
 	Nodes      []identity.Interface `json:"nodes"`
@@ -59,6 +156,46 @@ type IdentityEdge struct {
 	Node   identity.Interface `json:"node"`
 }
 
+type NewBugInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// "The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The title of the new bug.
+	Title string `json:"title"`
+	// The first message of the new bug.
+	Message string `json:"message"`
+	// The collection of file's hash required for the first message.
+	Files []git.Hash `json:"files"`
+}
+
+type NewBugPayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The created bug.
+	Bug *bug.Snapshot `json:"bug"`
+	// The resulting operation.
+	Operation *bug.CreateOperation `json:"operation"`
+}
+
+type OpenBugInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// "The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The bug ID's prefix.
+	Prefix string `json:"prefix"`
+}
+
+type OpenBugPayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The affected bug.
+	Bug *bug.Snapshot `json:"bug"`
+	// The resulting operation.
+	Operation *bug.SetStatusOperation `json:"operation"`
+}
+
 // The connection type for an Operation
 type OperationConnection struct {
 	Edges      []*OperationEdge `json:"edges"`
@@ -85,6 +222,26 @@ type PageInfo struct {
 	EndCursor string `json:"endCursor"`
 }
 
+type SetTitleInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// "The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The bug ID's prefix.
+	Prefix string `json:"prefix"`
+	// The new title.
+	Title string `json:"title"`
+}
+
+type SetTitlePayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The affected bug.
+	Bug *bug.Snapshot `json:"bug"`
+	// The resulting operation
+	Operation *bug.SetTitleOperation `json:"operation"`
+}
+
 // The connection type for TimelineItem
 type TimelineItemConnection struct {
 	Edges      []*TimelineItemEdge `json:"edges"`
@@ -99,6 +256,53 @@ type TimelineItemEdge struct {
 	Node   bug.TimelineItem `json:"node"`
 }
 
+type LabelChangeStatus string
+
+const (
+	LabelChangeStatusAdded         LabelChangeStatus = "ADDED"
+	LabelChangeStatusRemoved       LabelChangeStatus = "REMOVED"
+	LabelChangeStatusDuplicateInOp LabelChangeStatus = "DUPLICATE_IN_OP"
+	LabelChangeStatusAlreadyExist  LabelChangeStatus = "ALREADY_EXIST"
+	LabelChangeStatusDoesntExist   LabelChangeStatus = "DOESNT_EXIST"
+)
+
+var AllLabelChangeStatus = []LabelChangeStatus{
+	LabelChangeStatusAdded,
+	LabelChangeStatusRemoved,
+	LabelChangeStatusDuplicateInOp,
+	LabelChangeStatusAlreadyExist,
+	LabelChangeStatusDoesntExist,
+}
+
+func (e LabelChangeStatus) IsValid() bool {
+	switch e {
+	case LabelChangeStatusAdded, LabelChangeStatusRemoved, LabelChangeStatusDuplicateInOp, LabelChangeStatusAlreadyExist, LabelChangeStatusDoesntExist:
+		return true
+	}
+	return false
+}
+
+func (e LabelChangeStatus) String() string {
+	return string(e)
+}
+
+func (e *LabelChangeStatus) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = LabelChangeStatus(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid LabelChangeStatus", str)
+	}
+	return nil
+}
+
+func (e LabelChangeStatus) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
 type Status string
 
 const (

graphql/resolvers/label.go 🔗

@@ -2,10 +2,12 @@ package resolvers
 
 import (
 	"context"
+	"fmt"
 	"image/color"
 
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/graphql/graph"
+	"github.com/MichaelMure/git-bug/graphql/models"
 )
 
 var _ graph.LabelResolver = &labelResolver{}
@@ -20,3 +22,24 @@ func (labelResolver) Color(ctx context.Context, obj *bug.Label) (*color.RGBA, er
 	rgba := obj.RGBA()
 	return &rgba, nil
 }
+
+var _ graph.LabelChangeResultResolver = &labelChangeResultResolver{}
+
+type labelChangeResultResolver struct{}
+
+func (labelChangeResultResolver) Status(ctx context.Context, obj *bug.LabelChangeResult) (models.LabelChangeStatus, error) {
+	switch obj.Status {
+	case bug.LabelChangeAdded:
+		return models.LabelChangeStatusAdded, nil
+	case bug.LabelChangeRemoved:
+		return models.LabelChangeStatusRemoved, nil
+	case bug.LabelChangeDuplicateInOp:
+		return models.LabelChangeStatusDuplicateInOp, nil
+	case bug.LabelChangeAlreadySet:
+		return models.LabelChangeStatusAlreadyExist, nil
+	case bug.LabelChangeDoesntExist:
+		return models.LabelChangeStatusDoesntExist, nil
+	}
+
+	return "", fmt.Errorf("unknown status")
+}

graphql/resolvers/mutation.go 🔗

@@ -6,7 +6,7 @@ import (
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/graphql/graph"
-	"github.com/MichaelMure/git-bug/util/git"
+	"github.com/MichaelMure/git-bug/graphql/models"
 )
 
 var _ graph.MutationResolver = &mutationResolver{}
@@ -15,138 +15,193 @@ type mutationResolver struct {
 	cache *cache.MultiRepoCache
 }
 
-func (r mutationResolver) getRepo(repoRef *string) (*cache.RepoCache, error) {
-	if repoRef != nil {
-		return r.cache.ResolveRepo(*repoRef)
+func (r mutationResolver) getRepo(ref *string) (*cache.RepoCache, error) {
+	if ref != nil {
+		return r.cache.ResolveRepo(*ref)
 	}
 
 	return r.cache.DefaultRepo()
 }
 
-func (r mutationResolver) NewBug(ctx context.Context, repoRef *string, title string, message string, files []git.Hash) (*bug.Snapshot, error) {
-	repo, err := r.getRepo(repoRef)
+func (r mutationResolver) NewBug(ctx context.Context, input models.NewBugInput) (*models.NewBugPayload, error) {
+	repo, err := r.getRepo(input.RepoRef)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.NewBugWithFiles(title, message, files)
+	b, op, err := repo.NewBugWithFiles(input.Title, input.Message, input.Files)
 	if err != nil {
 		return nil, err
 	}
 
-	return b.Snapshot(), nil
+	return &models.NewBugPayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              b.Snapshot(),
+		Operation:        op,
+	}, nil
 }
 
-func (r mutationResolver) Commit(ctx context.Context, repoRef *string, prefix string) (*bug.Snapshot, error) {
-	repo, err := r.getRepo(repoRef)
+func (r mutationResolver) AddComment(ctx context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error) {
+	repo, err := r.getRepo(input.RepoRef)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(prefix)
+	b, err := repo.ResolveBugPrefix(input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	err = b.Commit()
+	op, err := b.AddCommentWithFiles(input.Message, input.Files)
+	if err != nil {
+		return nil, err
+	}
+
+	return &models.AddCommentPayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              b.Snapshot(),
+		Operation:        op,
+	}, nil
+}
+
+func (r mutationResolver) ChangeLabels(ctx context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) {
+	repo, err := r.getRepo(input.RepoRef)
+	if err != nil {
+		return nil, err
+	}
+
+	b, err := repo.ResolveBugPrefix(input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	return b.Snapshot(), nil
+	results, op, err := b.ChangeLabels(input.Added, input.Removed)
+	if err != nil {
+		return nil, err
+	}
+
+	resultsPtr := make([]*bug.LabelChangeResult, len(results))
+	for i, result := range results {
+		resultsPtr[i] = &result
+	}
+
+	return &models.ChangeLabelPayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              b.Snapshot(),
+		Operation:        op,
+		Results:          resultsPtr,
+	}, nil
 }
 
-func (r mutationResolver) AddComment(ctx context.Context, repoRef *string, prefix string, message string, files []git.Hash) (*bug.Snapshot, error) {
-	repo, err := r.getRepo(repoRef)
+func (r mutationResolver) OpenBug(ctx context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error) {
+	repo, err := r.getRepo(input.RepoRef)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(prefix)
+	b, err := repo.ResolveBugPrefix(input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	_, err = b.AddCommentWithFiles(message, files)
+	op, err := b.Open()
 	if err != nil {
 		return nil, err
 	}
 
-	return b.Snapshot(), nil
+	return &models.OpenBugPayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              b.Snapshot(),
+		Operation:        op,
+	}, nil
 }
 
-func (r mutationResolver) ChangeLabels(ctx context.Context, repoRef *string, prefix string, added []string, removed []string) (*bug.Snapshot, error) {
-	repo, err := r.getRepo(repoRef)
+func (r mutationResolver) CloseBug(ctx context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error) {
+	repo, err := r.getRepo(input.RepoRef)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(prefix)
+	b, err := repo.ResolveBugPrefix(input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	_, _, err = b.ChangeLabels(added, removed)
+	op, err := b.Close()
 	if err != nil {
 		return nil, err
 	}
 
-	return b.Snapshot(), nil
+	return &models.CloseBugPayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              b.Snapshot(),
+		Operation:        op,
+	}, nil
 }
 
-func (r mutationResolver) Open(ctx context.Context, repoRef *string, prefix string) (*bug.Snapshot, error) {
-	repo, err := r.getRepo(repoRef)
+func (r mutationResolver) SetTitle(ctx context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error) {
+	repo, err := r.getRepo(input.RepoRef)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(prefix)
+	b, err := repo.ResolveBugPrefix(input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	_, err = b.Open()
+	op, err := b.SetTitle(input.Title)
 	if err != nil {
 		return nil, err
 	}
 
-	return b.Snapshot(), nil
+	return &models.SetTitlePayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              b.Snapshot(),
+		Operation:        op,
+	}, nil
 }
 
-func (r mutationResolver) Close(ctx context.Context, repoRef *string, prefix string) (*bug.Snapshot, error) {
-	repo, err := r.getRepo(repoRef)
+func (r mutationResolver) Commit(ctx context.Context, input models.CommitInput) (*models.CommitPayload, error) {
+	repo, err := r.getRepo(input.RepoRef)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(prefix)
+	b, err := repo.ResolveBugPrefix(input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	_, err = b.Close()
+	err = b.Commit()
 	if err != nil {
 		return nil, err
 	}
 
-	return b.Snapshot(), nil
+	return &models.CommitPayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              b.Snapshot(),
+	}, nil
 }
 
-func (r mutationResolver) SetTitle(ctx context.Context, repoRef *string, prefix string, title string) (*bug.Snapshot, error) {
-	repo, err := r.getRepo(repoRef)
+func (r mutationResolver) CommitAsNeeded(ctx context.Context, input models.CommitAsNeededInput) (*models.CommitAsNeededPayload, error) {
+	repo, err := r.getRepo(input.RepoRef)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(prefix)
+	b, err := repo.ResolveBugPrefix(input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	_, err = b.SetTitle(title)
+	err = b.CommitAsNeeded()
 	if err != nil {
 		return nil, err
 	}
 
-	return b.Snapshot(), nil
+	return &models.CommitAsNeededPayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              b.Snapshot(),
+	}, nil
 }

graphql/resolvers/operations.go 🔗

@@ -63,5 +63,5 @@ func convertStatus(status bug.Status) (models.Status, error) {
 		return models.StatusClosed, nil
 	}
 
-	return "", fmt.Errorf("Unknown status")
+	return "", fmt.Errorf("unknown status")
 }

graphql/resolvers/query.go 🔗

@@ -27,8 +27,8 @@ func (r rootQueryResolver) DefaultRepository(ctx context.Context) (*models.Repos
 	}, nil
 }
 
-func (r rootQueryResolver) Repository(ctx context.Context, id string) (*models.Repository, error) {
-	repo, err := r.cache.ResolveRepo(id)
+func (r rootQueryResolver) Repository(ctx context.Context, ref string) (*models.Repository, error) {
+	repo, err := r.cache.ResolveRepo(ref)
 
 	if err != nil {
 		return nil, err

graphql/resolvers/root.go 🔗

@@ -30,6 +30,10 @@ func (r RootResolver) Mutation() graph.MutationResolver {
 	}
 }
 
+func (RootResolver) Repository() graph.RepositoryResolver {
+	return &repoResolver{}
+}
+
 func (RootResolver) Bug() graph.BugResolver {
 	return &bugResolver{}
 }
@@ -86,10 +90,6 @@ func (RootResolver) LabelChangeOperation() graph.LabelChangeOperationResolver {
 	return &labelChangeOperation{}
 }
 
-func (RootResolver) Repository() graph.RepositoryResolver {
-	return &repoResolver{}
-}
-
 func (RootResolver) SetStatusOperation() graph.SetStatusOperationResolver {
 	return &setStatusOperationResolver{}
 }
@@ -97,3 +97,7 @@ func (RootResolver) SetStatusOperation() graph.SetStatusOperationResolver {
 func (RootResolver) SetTitleOperation() graph.SetTitleOperationResolver {
 	return &setTitleOperationResolver{}
 }
+
+func (r RootResolver) LabelChangeResult() graph.LabelChangeResultResolver {
+	return &labelChangeResultResolver{}
+}

graphql/schema/mutations.graphql 🔗

@@ -0,0 +1,170 @@
+input NewBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The title of the new bug."""
+    title: String!
+    """The first message of the new bug."""
+    message: String!
+    """The collection of file's hash required for the first message."""
+    files: [Hash!]
+}
+
+type NewBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The created bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: CreateOperation!
+}
+
+input AddCommentInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The first message of the new bug."""
+    message: String!
+    """The collection of file's hash required for the first message."""
+    files: [Hash!]
+}
+
+type AddCommentPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: AddCommentOperation!
+}
+
+input ChangeLabelInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The list of label to add."""
+    added: [String!]
+    """The list of label to remove."""
+    Removed: [String!]
+}
+
+enum LabelChangeStatus {
+    ADDED
+    REMOVED
+    DUPLICATE_IN_OP
+    ALREADY_EXIST
+    DOESNT_EXIST
+}
+
+type LabelChangeResult {
+    """The source label."""
+    label: Label!
+    """The effect this label had."""
+    status: LabelChangeStatus!
+}
+
+type ChangeLabelPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: LabelChangeOperation!
+    """The effect each source label had."""
+    results: [LabelChangeResult]!
+}
+
+input OpenBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+}
+
+type OpenBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: SetStatusOperation!
+}
+
+input CloseBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+}
+
+type CloseBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation."""
+    operation: SetStatusOperation!
+}
+
+input SetTitleInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The new title."""
+    title: String!
+}
+
+type SetTitlePayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting operation"""
+    operation: SetTitleOperation!
+}
+
+input CommitInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+}
+
+type CommitPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+}
+
+input CommitAsNeededInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """"The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+}
+
+type CommitAsNeededPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+}

graphql/schema/root.graphql 🔗

@@ -1,16 +1,25 @@
 type Query {
+    """The default unnamend repository."""
     defaultRepository: Repository
-    repository(id: String!): Repository
+    """Access a repository by reference/name."""
+    repository(ref: String!): Repository
 }
 
 type Mutation {
-    newBug(repoRef: String, title: String!, message: String!, files: [Hash!]): Bug!
-
-    addComment(repoRef: String, prefix: String!, message: String!, files: [Hash!]): Bug!
-    changeLabels(repoRef: String, prefix: String!, added: [String!], removed: [String!]): Bug!
-    open(repoRef: String, prefix: String!): Bug!
-    close(repoRef: String, prefix: String!): Bug!
-    setTitle(repoRef: String, prefix: String!, title: String!): Bug!
-
-    commit(repoRef: String, prefix: String!): Bug!
+    """Create a new bug"""
+    newBug(input: NewBugInput!): NewBugPayload!
+    """Add a new comment to a bug"""
+    addComment(input: AddCommentInput!): AddCommentPayload!
+    """Add or remove a set of label on a bug"""
+    changeLabels(input: ChangeLabelInput): ChangeLabelPayload!
+    """Change a bug's status to open"""
+    openBug(input: OpenBugInput!): OpenBugPayload!
+    """Change a bug's status to closed"""
+    closeBug(input: CloseBugInput!): CloseBugPayload!
+    """Change a bug's title"""
+    setTitle(input: SetTitleInput!): SetTitlePayload!
+    """Commit write the pending operations into storage. This mutation fail if nothing is pending"""
+    commit(input: CommitInput!): CommitPayload!
+    """Commit write the pending operations into storage. This mutation succed if nothing is pending"""
+    commitAsNeeded(input: CommitAsNeededInput!): CommitAsNeededPayload!
 }

termui/termui.go 🔗

@@ -190,7 +190,7 @@ func newBugWithEditor(repo *cache.RepoCache) error {
 
 		return errTerminateMainloop
 	} else {
-		b, err = repo.NewBug(title, message)
+		b, _, err = repo.NewBug(title, message)
 		if err != nil {
 			return err
 		}