Repository: git-bug/git-bug (language: go)

Issue:
Import error: title is not fully printable
I am importing all the the issues from https://github.com/golang/go. (Currently I am using git-bug from PR #585).

I get the following error:
```
import error: issue creation: title is not fully printable
```
Here is the issue which causes that: https://github.com/golang/go/issues/43562.
The JSON from the GraphQL API looks as follows:
```
"issue": {
        "title": "src/net/http/header when header have\u0002 ",
        "body": "<!--\r\nPlease answer these questions before submitting your issue. Thanks!\r\nFor questions please use one of our forums: https://github.com/golang/go/wiki/Questions\r\n-->\r\n\r\n### What version of Go are you using (`go version`)?\r\n\r\n<pre>\r\n$ go version\r\ngo version go1.15.2 linux/amd64\r\n</pre>\r\n\r\nwhen i post header like (202012300001\u0002sometext\u0002timestampvalue)  to go http server   \r\ni got error  400 bad request  invalid header value  \r\n\r\n\r\n"
}
```


Discussion / hints:
So what's happening is that the core of git-bug is checking that there is no funny characters in the bugs (here it's a control character). This is checked with the function `text.Safe()`.

There is the counterpart function `text.Cleanup()` that should be used on every imported text by the bridges. In the github bridge, this is done for the content of the issue, but not the title.
On that topic, I found that `text.Cleanup()` can't return an error with how the current code is written, so this could be made way cleaner.

Fix patch:
diff --git a/api/graphql/resolvers/mutation.go b/api/graphql/resolvers/mutation.go
index 9cd936a68..00c9e3c12 100644
--- a/api/graphql/resolvers/mutation.go
+++ b/api/graphql/resolvers/mutation.go
@@ -5,11 +5,12 @@ import (
 	"time"
 
 	"github.com/MichaelMure/git-bug/api/auth"
-	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/api/graphql/graph"
 	"github.com/MichaelMure/git-bug/api/graphql/models"
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 var _ graph.MutationResolver = &mutationResolver{}
@@ -50,7 +51,12 @@ func (r mutationResolver) NewBug(ctx context.Context, input models.NewBugInput)
 		return nil, err
 	}
 
-	b, op, err := repo.NewBugRaw(author, time.Now().Unix(), input.Title, input.Message, input.Files, nil)
+	b, op, err := repo.NewBugRaw(author,
+		time.Now().Unix(),
+		text.CleanupOneLine(input.Title),
+		text.Cleanup(input.Message),
+		input.Files,
+		nil)
 	if err != nil {
 		return nil, err
 	}
@@ -73,7 +79,11 @@ func (r mutationResolver) AddComment(ctx context.Context, input models.AddCommen
 		return nil, err
 	}
 
-	op, err := b.AddCommentRaw(author, time.Now().Unix(), input.Message, input.Files, nil)
+	op, err := b.AddCommentRaw(author,
+		time.Now().Unix(),
+		text.Cleanup(input.Message),
+		input.Files,
+		nil)
 	if err != nil {
 		return nil, err
 	}
@@ -101,7 +111,13 @@ func (r mutationResolver) EditComment(ctx context.Context, input models.EditComm
 		return nil, err
 	}
 
-	op, err := b.EditCommentRaw(author, time.Now().Unix(), entity.Id(input.Target), input.Message, nil)
+	op, err := b.EditCommentRaw(
+		author,
+		time.Now().Unix(),
+		entity.Id(input.Target),
+		text.Cleanup(input.Message),
+		nil,
+	)
 	if err != nil {
 		return nil, err
 	}
@@ -129,7 +145,13 @@ func (r mutationResolver) ChangeLabels(ctx context.Context, input *models.Change
 		return nil, err
 	}
 
-	results, op, err := b.ChangeLabelsRaw(author, time.Now().Unix(), input.Added, input.Removed, nil)
+	results, op, err := b.ChangeLabelsRaw(
+		author,
+		time.Now().Unix(),
+		text.CleanupOneLineArray(input.Added),
+		text.CleanupOneLineArray(input.Removed),
+		nil,
+	)
 	if err != nil {
 		return nil, err
 	}
@@ -219,7 +241,12 @@ func (r mutationResolver) SetTitle(ctx context.Context, input models.SetTitleInp
 		return nil, err
 	}
 
-	op, err := b.SetTitleRaw(author, time.Now().Unix(), input.Title, nil)
+	op, err := b.SetTitleRaw(
+		author,
+		time.Now().Unix(),
+		text.CleanupOneLine(input.Title),
+		nil,
+	)
 	if err != nil {
 		return nil, err
 	}
diff --git a/bridge/github/import.go b/bridge/github/import.go
index bf43a8770..f410cc651 100644
--- a/bridge/github/import.go
+++ b/bridge/github/import.go
@@ -211,17 +211,13 @@ func (gi *githubImporter) ensureIssue(ctx context.Context, repo *cache.RepoCache
 		// if there are no issue edits then the issue struct holds the bug creation
 		textInput = string(issue.Body)
 	}
-	cleanText, err := text.Cleanup(textInput)
-	if err != nil {
-		return nil, err
-	}
 
 	// create bug
 	b, _, err = repo.NewBugRaw(
 		author,
 		issue.CreatedAt.Unix(),
-		title, // TODO: this is the *current* title, not the original one
-		cleanText,
+		text.CleanupOneLine(title), // TODO: this is the *current* title, not the original one
+		text.Cleanup(textInput),
 		nil,
 		map[string]string{
 			core.MetaKeyOrigin: target,
@@ -269,7 +265,7 @@ func (gi *githubImporter) ensureTimelineItem(ctx context.Context, repo *cache.Re
 			author,
 			item.LabeledEvent.CreatedAt.Unix(),
 			[]string{
-				string(item.LabeledEvent.Label.Name),
+				text.CleanupOneLine(string(item.LabeledEvent.Label.Name)),
 			},
 			nil,
 			map[string]string{metaKeyGithubId: id},
@@ -300,7 +296,7 @@ func (gi *githubImporter) ensureTimelineItem(ctx context.Context, repo *cache.Re
 			item.UnlabeledEvent.CreatedAt.Unix(),
 			nil,
 			[]string{
-				string(item.UnlabeledEvent.Label.Name),
+				text.CleanupOneLine(string(item.UnlabeledEvent.Label.Name)),
 			},
 			map[string]string{metaKeyGithubId: id},
 		)
@@ -382,7 +378,7 @@ func (gi *githubImporter) ensureTimelineItem(ctx context.Context, repo *cache.Re
 		// The title provided by the GraphQL API actually consists of a space followed
 		// by a zero width space (U+200B). This title would cause the NewBugRaw()
 		// function to return an error: empty title.
-		title := string(item.RenamedTitleEvent.CurrentTitle)
+		title := text.CleanupOneLine(string(item.RenamedTitleEvent.CurrentTitle))
 		if title == " \u200b" { // U+200B == zero width space
 			title = EmptyTitlePlaceholder
 		}
@@ -429,17 +425,12 @@ func (gi *githubImporter) ensureCommentEdit(ctx context.Context, repo *cache.Rep
 		return nil
 	}
 
-	cleanText, err := text.Cleanup(string(*edit.Diff))
-	if err != nil {
-		return err
-	}
-
 	// comment edition
 	op, err := b.EditCommentRaw(
 		editor,
 		edit.CreatedAt.Unix(),
 		target,
-		cleanText,
+		text.Cleanup(string(*edit.Diff)),
 		map[string]string{
 			metaKeyGithubId: parseId(edit.Id),
 		},
@@ -476,16 +467,12 @@ func (gi *githubImporter) ensureComment(ctx context.Context, repo *cache.RepoCac
 		// if there are not comment edits, then the comment struct holds the comment creation
 		textInput = string(comment.Body)
 	}
-	cleanText, err := text.Cleanup(textInput)
-	if err != nil {
-		return err
-	}
 
 	// add comment operation
 	op, err := b.AddCommentRaw(
 		author,
 		comment.CreatedAt.Unix(),
-		cleanText,
+		text.Cleanup(textInput),
 		nil,
 		map[string]string{
 			metaKeyGithubId:  parseId(comment.Id),
diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go
index 7939f4e48..cc99c12ef 100644
--- a/bridge/gitlab/import.go
+++ b/bridge/gitlab/import.go
@@ -137,22 +137,12 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue
 		return nil, err
 	}
 
-	// if bug was never imported
-	cleanTitle, err := text.Cleanup(issue.Title)
-	if err != nil {
-		return nil, err
-	}
-	cleanDesc, err := text.Cleanup(issue.Description)
-	if err != nil {
-		return nil, err
-	}
-
-	// create bug
+	// if bug was never imported, create bug
 	b, _, err = repo.NewBugRaw(
 		author,
 		issue.CreatedAt.Unix(),
-		cleanTitle,
-		cleanDesc,
+		text.CleanupOneLine(issue.Title),
+		text.Cleanup(issue.Description),
 		nil,
 		map[string]string{
 			core.MetaKeyOrigin:   target,
@@ -238,7 +228,7 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
 				author,
 				note.UpdatedAt.Unix(),
 				firstComment.Id(),
-				issue.Description,
+				text.Cleanup(issue.Description),
 				map[string]string{
 					metaKeyGitlabId: gitlabID,
 				},
@@ -251,10 +241,7 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
 		}
 
 	case NOTE_COMMENT:
-		cleanText, err := text.Cleanup(body)
-		if err != nil {
-			return err
-		}
+		cleanText := text.Cleanup(body)
 
 		// if we didn't import the comment
 		if errResolve == cache.ErrNoMatchingOp {
@@ -312,7 +299,7 @@ func (gi *gitlabImporter) ensureNote(repo *cache.RepoCache, b *cache.BugCache, n
 		op, err := b.SetTitleRaw(
 			author,
 			note.CreatedAt.Unix(),
-			body,
+			text.CleanupOneLine(body),
 			map[string]string{
 				metaKeyGitlabId: gitlabID,
 			},
@@ -361,7 +348,7 @@ func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCa
 		_, err = b.ForceChangeLabelsRaw(
 			author,
 			labelEvent.CreatedAt.Unix(),
-			[]string{labelEvent.Label.Name},
+			[]string{text.CleanupOneLine(labelEvent.Label.Name)},
 			nil,
 			map[string]string{
 				metaKeyGitlabId: parseID(labelEvent.ID),
@@ -373,7 +360,7 @@ func (gi *gitlabImporter) ensureLabelEvent(repo *cache.RepoCache, b *cache.BugCa
 			author,
 			labelEvent.CreatedAt.Unix(),
 			nil,
-			[]string{labelEvent.Label.Name},
+			[]string{text.CleanupOneLine(labelEvent.Label.Name)},
 			map[string]string{
 				metaKeyGitlabId: parseID(labelEvent.ID),
 			},
diff --git a/bridge/jira/import.go b/bridge/jira/import.go
index 00148bb62..d601febba 100644
--- a/bridge/jira/import.go
+++ b/bridge/jira/import.go
@@ -232,19 +232,11 @@ func (ji *jiraImporter) ensureIssue(repo *cache.RepoCache, issue Issue) (*cache.
 	}
 
 	if err == bug.ErrBugNotExist {
-		cleanText, err := text.Cleanup(string(issue.Fields.Description))
-		if err != nil {
-			return nil, err
-		}
-
-		// NOTE(josh): newlines in titles appears to be rare, but it has been seen
-		// in the wild. It does not appear to be allowed in the JIRA web interface.
-		title := strings.Replace(issue.Fields.Summary, "\n", "", -1)
 		b, _, err = repo.NewBugRaw(
 			author,
 			issue.Fields.Created.Unix(),
-			title,
-			cleanText,
+			text.CleanupOneLine(issue.Fields.Summary),
+			text.Cleanup(issue.Fields.Description),
 			nil,
 			map[string]string{
 				core.MetaKeyOrigin: target,
@@ -289,10 +281,7 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 			// We don't know the original text... we only have the updated text.
 			cleanText = ""
 		} else {
-			cleanText, err = text.Cleanup(string(item.Body))
-			if err != nil {
-				return err
-			}
+			cleanText = text.Cleanup(item.Body)
 		}
 
 		// add comment operation
@@ -340,15 +329,11 @@ func (ji *jiraImporter) ensureComment(repo *cache.RepoCache, b *cache.BugCache,
 	}
 
 	// comment edition
-	cleanText, err := text.Cleanup(string(item.Body))
-	if err != nil {
-		return err
-	}
 	op, err := b.EditCommentRaw(
 		editor,
 		item.Updated.Unix(),
 		targetOpID,
-		cleanText,
+		text.Cleanup(item.Body),
 		map[string]string{
 			metaKeyJiraId: derivedID,
 		},
@@ -513,8 +498,8 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			op, err := b.ForceChangeLabelsRaw(
 				author,
 				entry.Created.Unix(),
-				addedLabels,
-				removedLabels,
+				text.CleanupOneLineArray(addedLabels),
+				text.CleanupOneLineArray(removedLabels),
 				map[string]string{
 					metaKeyJiraId:        entry.ID,
 					metaKeyJiraDerivedId: derivedID,
@@ -571,7 +556,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			op, err := b.SetTitleRaw(
 				author,
 				entry.Created.Unix(),
-				string(item.ToString),
+				text.CleanupOneLine(item.ToString),
 				map[string]string{
 					metaKeyJiraId:        entry.ID,
 					metaKeyJiraDerivedId: derivedID,
@@ -589,7 +574,7 @@ func (ji *jiraImporter) ensureChange(repo *cache.RepoCache, b *cache.BugCache, e
 			op, err := b.EditCreateCommentRaw(
 				author,
 				entry.Created.Unix(),
-				string(item.ToString),
+				text.Cleanup(item.ToString),
 				map[string]string{
 					metaKeyJiraId:        entry.ID,
 					metaKeyJiraDerivedId: derivedID,
diff --git a/bridge/launchpad/import.go b/bridge/launchpad/import.go
index 6b5667bae..01f6e53b8 100644
--- a/bridge/launchpad/import.go
+++ b/bridge/launchpad/import.go
@@ -9,6 +9,7 @@ import (
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 type launchpadImporter struct {
@@ -83,8 +84,8 @@ func (li *launchpadImporter) ImportAll(ctx context.Context, repo *cache.RepoCach
 					b, _, err = repo.NewBugRaw(
 						owner,
 						createdAt.Unix(),
-						lpBug.Title,
-						lpBug.Description,
+						text.CleanupOneLine(lpBug.Title),
+						text.Cleanup(lpBug.Description),
 						nil,
 						map[string]string{
 							core.MetaKeyOrigin: target,
@@ -133,7 +134,7 @@ func (li *launchpadImporter) ImportAll(ctx context.Context, repo *cache.RepoCach
 					op, err := b.AddCommentRaw(
 						owner,
 						createdAt.Unix(),
-						lpMessage.Content,
+						text.Cleanup(lpMessage.Content),
 						nil,
 						map[string]string{
 							metaKeyLaunchpadID: lpMessage.ID,
diff --git a/bug/label.go b/bug/label.go
index 9d1e99a74..79b5f5911 100644
--- a/bug/label.go
+++ b/bug/label.go
@@ -4,7 +4,6 @@ import (
 	"crypto/sha256"
 	"fmt"
 	"image/color"
-	"strings"
 
 	fcolor "github.com/fatih/color"
 
@@ -58,12 +57,8 @@ func (l Label) Validate() error {
 		return fmt.Errorf("empty")
 	}
 
-	if strings.Contains(str, "\n") {
-		return fmt.Errorf("should be a single line")
-	}
-
-	if !text.Safe(str) {
-		return fmt.Errorf("not fully printable")
+	if !text.SafeOneLine(str) {
+		return fmt.Errorf("label has unsafe characters")
 	}
 
 	return nil
diff --git a/bug/op_create.go b/bug/op_create.go
index 75b60bd8b..1d01020cf 100644
--- a/bug/op_create.go
+++ b/bug/op_create.go
@@ -3,7 +3,6 @@ package bug
 import (
 	"encoding/json"
 	"fmt"
-	"strings"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/entity/dag"
@@ -94,11 +93,8 @@ func (op *CreateOperation) Validate() error {
 	if text.Empty(op.Title) {
 		return fmt.Errorf("title is empty")
 	}
-	if strings.Contains(op.Title, "\n") {
-		return fmt.Errorf("title should be a single line")
-	}
-	if !text.Safe(op.Title) {
-		return fmt.Errorf("title is not fully printable")
+	if !text.SafeOneLine(op.Title) {
+		return fmt.Errorf("title has unsafe characters")
 	}
 
 	if !text.Safe(op.Message) {
diff --git a/bug/op_set_metadata.go b/bug/op_set_metadata.go
index ca19a8384..28496fd85 100644
--- a/bug/op_set_metadata.go
+++ b/bug/op_set_metadata.go
@@ -2,11 +2,13 @@ package bug
 
 import (
 	"encoding/json"
+	"fmt"
 
 	"github.com/pkg/errors"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 var _ Operation = &SetMetadataOperation{}
@@ -43,6 +45,15 @@ func (op *SetMetadataOperation) Validate() error {
 		return errors.Wrap(err, "target invalid")
 	}
 
+	for key, val := range op.NewMetadata {
+		if !text.SafeOneLine(key) {
+			return fmt.Errorf("metadata key is unsafe")
+		}
+		if !text.Safe(val) {
+			return fmt.Errorf("metadata value is not fully printable")
+		}
+	}
+
 	return nil
 }
 
diff --git a/bug/op_set_title.go b/bug/op_set_title.go
index c6a26746c..badd192c9 100644
--- a/bug/op_set_title.go
+++ b/bug/op_set_title.go
@@ -3,7 +3,6 @@ package bug
 import (
 	"encoding/json"
 	"fmt"
-	"strings"
 
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/identity"
@@ -49,20 +48,12 @@ func (op *SetTitleOperation) Validate() error {
 		return fmt.Errorf("title is empty")
 	}
 
-	if strings.Contains(op.Title, "\n") {
-		return fmt.Errorf("title should be a single line")
+	if !text.SafeOneLine(op.Title) {
+		return fmt.Errorf("title has unsafe characters")
 	}
 
-	if !text.Safe(op.Title) {
-		return fmt.Errorf("title should be fully printable")
-	}
-
-	if strings.Contains(op.Was, "\n") {
-		return fmt.Errorf("previous title should be a single line")
-	}
-
-	if !text.Safe(op.Was) {
-		return fmt.Errorf("previous title should be fully printable")
+	if !text.SafeOneLine(op.Was) {
+		return fmt.Errorf("previous title has unsafe characters")
 	}
 
 	return nil
diff --git a/cache/bug_cache.go b/cache/bug_cache.go
index bbe9830f9..a248e872a 100644
--- a/cache/bug_cache.go
+++ b/cache/bug_cache.go
@@ -235,6 +235,7 @@ func (c *BugCache) SetTitleRaw(author *IdentityCache, unixTime int64, title stri
 	return op, c.notifyUpdated()
 }
 
+// Convenience function to edit the body of a bug (the first comment)
 func (c *BugCache) EditCreateComment(body string) (*bug.EditCommentOperation, error) {
 	author, err := c.repoCache.GetUserIdentity()
 	if err != nil {
@@ -244,6 +245,7 @@ func (c *BugCache) EditCreateComment(body string) (*bug.EditCommentOperation, er
 	return c.EditCreateCommentRaw(author, time.Now().Unix(), body, nil)
 }
 
+// Convenience function to edit the body of a bug (the first comment)
 func (c *BugCache) EditCreateCommentRaw(author *IdentityCache, unixTime int64, body string, metadata map[string]string) (*bug.EditCommentOperation, error) {
 	c.mu.Lock()
 	op, err := bug.EditCreateComment(c.bug, author.Identity, unixTime, body)
diff --git a/commands/add.go b/commands/add.go
index 17fbbc935..ad3b164b3 100644
--- a/commands/add.go
+++ b/commands/add.go
@@ -4,6 +4,7 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 type addOptions struct {
@@ -60,7 +61,10 @@ func runAdd(env *Env, opts addOptions) error {
 		}
 	}
 
-	b, _, err := env.backend.NewBug(opts.title, opts.message)
+	b, _, err := env.backend.NewBug(
+		text.CleanupOneLine(opts.title),
+		text.Cleanup(opts.message),
+	)
 	if err != nil {
 		return err
 	}
diff --git a/commands/comment_add.go b/commands/comment_add.go
index 6caf49436..438e983a3 100644
--- a/commands/comment_add.go
+++ b/commands/comment_add.go
@@ -5,6 +5,7 @@ import (
 
 	_select "github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 type commentAddOptions struct {
@@ -62,7 +63,7 @@ func runCommentAdd(env *Env, opts commentAddOptions, args []string) error {
 		}
 	}
 
-	_, err = b.AddComment(opts.message)
+	_, err = b.AddComment(text.Cleanup(opts.message))
 	if err != nil {
 		return err
 	}
diff --git a/commands/label_add.go b/commands/label_add.go
index ce971f043..30c0d77f9 100644
--- a/commands/label_add.go
+++ b/commands/label_add.go
@@ -4,6 +4,7 @@ import (
 	"github.com/spf13/cobra"
 
 	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 func newLabelAddCommand() *cobra.Command {
@@ -30,7 +31,7 @@ func runLabelAdd(env *Env, args []string) error {
 
 	added := args
 
-	changes, _, err := b.ChangeLabels(added, nil)
+	changes, _, err := b.ChangeLabels(text.CleanupOneLineArray(added), nil)
 
 	for _, change := range changes {
 		env.out.Println(change)
diff --git a/commands/label_rm.go b/commands/label_rm.go
index f59ec02d8..9fb3e0967 100644
--- a/commands/label_rm.go
+++ b/commands/label_rm.go
@@ -4,6 +4,7 @@ import (
 	"github.com/spf13/cobra"
 
 	_select "github.com/MichaelMure/git-bug/commands/select"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 func newLabelRmCommand() *cobra.Command {
@@ -30,7 +31,7 @@ func runLabelRm(env *Env, args []string) error {
 
 	removed := args
 
-	changes, _, err := b.ChangeLabels(nil, removed)
+	changes, _, err := b.ChangeLabels(nil, text.CleanupOneLineArray(removed))
 
 	for _, change := range changes {
 		env.out.Println(change)
diff --git a/commands/title_edit.go b/commands/title_edit.go
index 3750f2cc0..455c93842 100644
--- a/commands/title_edit.go
+++ b/commands/title_edit.go
@@ -5,6 +5,7 @@ import (
 
 	_select "github.com/MichaelMure/git-bug/commands/select"
 	"github.com/MichaelMure/git-bug/input"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 type titleEditOptions struct {
@@ -58,7 +59,7 @@ func runTitleEdit(env *Env, opts titleEditOptions, args []string) error {
 		env.err.Println("No change, aborting.")
 	}
 
-	_, err = b.SetTitle(opts.title)
+	_, err = b.SetTitle(text.CleanupOneLine(opts.title))
 	if err != nil {
 		return err
 	}
diff --git a/identity/version.go b/identity/version.go
index 1c35831e0..9a52d0894 100644
--- a/identity/version.go
+++ b/identity/version.go
@@ -4,7 +4,6 @@ import (
 	"crypto/rand"
 	"encoding/json"
 	"fmt"
-	"strings"
 	"time"
 
 	"github.com/pkg/errors"
@@ -186,25 +185,16 @@ func (v *version) Validate() error {
 	if text.Empty(v.name) && text.Empty(v.login) {
 		return fmt.Errorf("either name or login should be set")
 	}
-	if strings.Contains(v.name, "\n") {
-		return fmt.Errorf("name should be a single line")
-	}
-	if !text.Safe(v.name) {
-		return fmt.Errorf("name is not fully printable")
+	if !text.SafeOneLine(v.name) {
+		return fmt.Errorf("name has unsafe characters")
 	}
 
-	if strings.Contains(v.login, "\n") {
-		return fmt.Errorf("login should be a single line")
-	}
-	if !text.Safe(v.login) {
-		return fmt.Errorf("login is not fully printable")
+	if !text.SafeOneLine(v.login) {
+		return fmt.Errorf("login has unsafe characters")
 	}
 
-	if strings.Contains(v.email, "\n") {
-		return fmt.Errorf("email should be a single line")
-	}
-	if !text.Safe(v.email) {
-		return fmt.Errorf("email is not fully printable")
+	if !text.SafeOneLine(v.email) {
+		return fmt.Errorf("email has unsafe characters")
 	}
 
 	if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) {
diff --git a/termui/termui.go b/termui/termui.go
index ec5387a49..3e7f43b93 100644
--- a/termui/termui.go
+++ b/termui/termui.go
@@ -11,6 +11,7 @@ import (
 	"github.com/MichaelMure/git-bug/entity"
 	"github.com/MichaelMure/git-bug/input"
 	"github.com/MichaelMure/git-bug/query"
+	"github.com/MichaelMure/git-bug/util/text"
 )
 
 var errTerminateMainloop = errors.New("terminate gocui mainloop")
@@ -199,7 +200,10 @@ func newBugWithEditor(repo *cache.RepoCache) error {
 
 		return errTerminateMainloop
 	} else {
-		b, _, err = repo.NewBug(title, message)
+		b, _, err = repo.NewBug(
+			text.CleanupOneLine(title),
+			text.Cleanup(message),
+		)
 		if err != nil {
 			return err
 		}
@@ -235,7 +239,7 @@ func addCommentWithEditor(bug *cache.BugCache) error {
 	if err == input.ErrEmptyMessage {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "Empty message, aborting.")
 	} else {
-		_, err := bug.AddComment(message)
+		_, err := bug.AddComment(text.Cleanup(message))
 		if err != nil {
 			return err
 		}
@@ -270,7 +274,7 @@ func editCommentWithEditor(bug *cache.BugCache, target entity.Id, preMessage str
 	} else if message == preMessage {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "No changes found, aborting.")
 	} else {
-		_, err := bug.EditComment(target, message)
+		_, err := bug.EditComment(target, text.Cleanup(message))
 		if err != nil {
 			return err
 		}
@@ -307,7 +311,7 @@ func setTitleWithEditor(bug *cache.BugCache) error {
 	} else if title == snap.Title {
 		ui.msgPopup.Activate(msgPopupErrorTitle, "No change, aborting.")
 	} else {
-		_, err := bug.SetTitle(title)
+		_, err := bug.SetTitle(text.CleanupOneLine(title))
 		if err != nil {
 			return err
 		}
diff --git a/util/text/transform.go b/util/text/transform.go
index 59dc4e032..395a57be6 100644
--- a/util/text/transform.go
+++ b/util/text/transform.go
@@ -8,7 +8,7 @@ import (
 	"golang.org/x/text/transform"
 )
 
-func Cleanup(text string) (string, error) {
+func Cleanup(text string) string {
 	// windows new line, Github, really ?
 	text = strings.Replace(text, "\r\n", "\n", -1)
 
@@ -23,9 +23,33 @@ func Cleanup(text string) (string, error) {
 	}))
 	sanitized, _, err := transform.String(t, text)
 	if err != nil {
-		return "", err
+		// transform.String should never return an error as our transformer doesn't returns one.
+		// Confirmed with fuzzing.
+		panic(err)
 	}
 
 	// trim extra new line not displayed in the github UI but still present in the data
-	return strings.TrimSpace(sanitized), nil
+	return strings.TrimSpace(sanitized)
+}
+
+func CleanupOneLine(text string) string {
+	// remove all unicode control characters *including*
+	// '\n', '\r' and '\t'
+	t := runes.Remove(runes.Predicate(unicode.IsControl))
+	sanitized, _, err := transform.String(t, text)
+	if err != nil {
+		// transform.String should never return an error as our transformer doesn't returns one.
+		// Confirmed with fuzzing.
+		panic(err)
+	}
+
+	// trim extra new line not displayed in the github UI but still present in the data
+	return strings.TrimSpace(sanitized)
+}
+
+func CleanupOneLineArray(texts []string) []string {
+	for i := range texts {
+		texts[i] = CleanupOneLine(texts[i])
+	}
+	return texts
 }
diff --git a/util/text/validate.go b/util/text/validate.go
index 51e94fb4b..4c3f7065b 100644
--- a/util/text/validate.go
+++ b/util/text/validate.go
@@ -33,6 +33,18 @@ func Safe(s string) bool {
 	return true
 }
 
+// Safe will tell if a character in the string is considered unsafe
+// Currently trigger on all unicode control character
+func SafeOneLine(s string) bool {
+	for _, r := range s {
+		if unicode.IsControl(r) {
+			return false
+		}
+	}
+
+	return true
+}
+
 // ValidUrl will tell if the string contains what seems to be a valid URL
 func ValidUrl(s string) bool {
 	if strings.Contains(s, "\n") {


Test patch: