From 0424a0fa19cbd2d2110d748a0ff3c014615aa17e Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 8 Jun 2019 23:14:35 +0200 Subject: [PATCH 01/29] [commands] bridge: add push sub command [commands] bridge: use cobra max args --- commands/bridge_pull.go | 4 +++- commands/bridge_push.go | 53 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 commands/bridge_push.go diff --git a/commands/bridge_pull.go b/commands/bridge_pull.go index c7a22d6da712956a24d87875f49f9fa06cb09f54..2edabfaf9a87359f10fd321f6d1ad24cce484256 100644 --- a/commands/bridge_pull.go +++ b/commands/bridge_pull.go @@ -3,11 +3,12 @@ package commands import ( "time" + "github.com/spf13/cobra" + "github.com/MichaelMure/git-bug/bridge" "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/util/interrupt" - "github.com/spf13/cobra" ) func runBridgePull(cmd *cobra.Command, args []string) error { @@ -44,6 +45,7 @@ var bridgePullCmd = &cobra.Command{ Short: "Pull updates.", PreRunE: loadRepo, RunE: runBridgePull, + Args: cobra.MaximumNArgs(1), } func init() { diff --git a/commands/bridge_push.go b/commands/bridge_push.go new file mode 100644 index 0000000000000000000000000000000000000000..19b5fc4f2d562be99caadf269f4bea70a5b3cbf9 --- /dev/null +++ b/commands/bridge_push.go @@ -0,0 +1,53 @@ +package commands + +import ( + "time" + + "github.com/spf13/cobra" + + "github.com/MichaelMure/git-bug/bridge" + "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/util/interrupt" +) + +func runBridgePush(cmd *cobra.Command, args []string) error { + backend, err := cache.NewRepoCache(repo) + if err != nil { + return err + } + defer backend.Close() + interrupt.RegisterCleaner(backend.Close) + + var b *core.Bridge + + if len(args) == 0 { + b, err = bridge.DefaultBridge(backend) + } else { + b, err = bridge.NewBridgeFromFullName(backend, args[0]) + } + + if err != nil { + return err + } + + // TODO: by default export only new events + err = b.ExportAll(time.Time{}) + if err != nil { + return err + } + + return nil +} + +var bridgePushCmd = &cobra.Command{ + Use: "push []", + Short: "Push updates.", + PreRunE: loadRepo, + RunE: runBridgePush, + Args: cobra.MaximumNArgs(1), +} + +func init() { + bridgeCmd.AddCommand(bridgePushCmd) +} From fbb0e836d3d23e5a3e47029c92426090e21615f7 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 8 Jun 2019 23:14:59 +0200 Subject: [PATCH 02/29] [bridge/github] Add exporter implementation --- bridge/github/export.go | 49 ++++++++++++++++++++++++++++++++++++ bridge/github/export_test.go | 7 ++++++ bridge/github/github.go | 2 +- 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 bridge/github/export.go create mode 100644 bridge/github/export_test.go diff --git a/bridge/github/export.go b/bridge/github/export.go new file mode 100644 index 0000000000000000000000000000000000000000..3c07d682c0e9d3c8dff8ef3ba3acba5e9312373d --- /dev/null +++ b/bridge/github/export.go @@ -0,0 +1,49 @@ +package github + +import ( + "time" + + "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/cache" +) + +// githubImporter implement the Importer interface +type githubExporter struct { + conf core.Configuration +} + +func (ge *githubExporter) Init(conf core.Configuration) error { + ge.conf = conf + return nil +} + +// ExportAll export all event made by the current user to Github +func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) error { + identity, err := repo.GetUserIdentity() + if err != nil { + return err + } + + allBugsIds := repo.AllBugsIds() + + // + bugs := make([]*cache.BugCache, 0) + for _, id := range allBugsIds { + b, err := repo.ResolveBug(id) + if err != nil { + return err + } + + // check if user participated in the issue + participants := b.Snapshot().Participants + for _, p := range participants { + if p.Id() == identity.Id() { + bugs = append(bugs, b) + } + } + } + + //TODO: Export bugs/events/editions + + return nil +} diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go new file mode 100644 index 0000000000000000000000000000000000000000..74e8a656df9a7b9ccfd84cf4c93ceb65ae8229f6 --- /dev/null +++ b/bridge/github/export_test.go @@ -0,0 +1,7 @@ +package github + +import "testing" + +func TestExporter(t *testing.T) { + +} diff --git a/bridge/github/github.go b/bridge/github/github.go index 3e717ee977c8915c622c5fd543445ec60139278e..46004dc85757d89c92adf303ce2811810c5bdafe 100644 --- a/bridge/github/github.go +++ b/bridge/github/github.go @@ -25,7 +25,7 @@ func (*Github) NewImporter() core.Importer { } func (*Github) NewExporter() core.Exporter { - return nil + return &githubExporter{} } func buildClient(token string) *githubv4.Client { From 946c936c5c447fd1ef36fca0a17c6e6ec40c8054 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Fri, 14 Jun 2019 20:27:17 +0200 Subject: [PATCH 03/29] [vendor] update dependencies: github.com/shurcooL/githubv4 [bridge/github] graphql: Add more mutations structs --- Gopkg.lock | 4 +- .../github.com/shurcooL/githubv4/.travis.yml | 2 +- vendor/github.com/shurcooL/githubv4/README.md | 8 +- vendor/github.com/shurcooL/githubv4/enum.go | 298 ++++++++- vendor/github.com/shurcooL/githubv4/gen.go | 13 +- vendor/github.com/shurcooL/githubv4/input.go | 574 +++++++++++++++++- 6 files changed, 867 insertions(+), 32 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 2bb58e6cb08fac725f632f31d580cb69b78687e4..11cf2f130219011fabda4be9c660ad8388361463 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -214,11 +214,11 @@ [[projects]] branch = "master" - digest = "1:a1562fb3021983f533a27d02ebf36f1bc1ab327660d611d6e948970b54087792" + digest = "1:2befa342040f385b214cfd400887b584d5eba4e4b25a0ebaea839ddb0d59c586" name = "github.com/shurcooL/githubv4" packages = ["."] pruneopts = "UT" - revision = "b5f70540eee0ebfb6a27b52fc5b131be76415539" + revision = "068505affed7d8555196a48eb3e0ed43410aa8e8" [[projects]] branch = "master" diff --git a/vendor/github.com/shurcooL/githubv4/.travis.yml b/vendor/github.com/shurcooL/githubv4/.travis.yml index 93b1fcdb31a27fd7c428a071d66d92289e598b62..437c57db3d00d3c9d3dd9ad2717bcb9c79d2bd7a 100644 --- a/vendor/github.com/shurcooL/githubv4/.travis.yml +++ b/vendor/github.com/shurcooL/githubv4/.travis.yml @@ -12,5 +12,5 @@ install: script: - go get -t -v ./... - diff -u <(echo -n) <(gofmt -d -s .) - - go tool vet . + - go vet ./... - go test -v -race ./... diff --git a/vendor/github.com/shurcooL/githubv4/README.md b/vendor/github.com/shurcooL/githubv4/README.md index 0d389ef095ae8309aa5ba34a9ef76358220fdc60..319a6428ab6ab7271d281c215da4d24b5a0d6b64 100644 --- a/vendor/github.com/shurcooL/githubv4/README.md +++ b/vendor/github.com/shurcooL/githubv4/README.md @@ -48,7 +48,7 @@ func main() { ### Simple Query -To make a query, you need to define a Go type that corresponds to the GitHub GraphQL schema, and contains the fields you're interested in querying. You can look up the GitHub GraphQL schema at https://developer.github.com/v4/reference/query/. +To make a query, you need to define a Go type that corresponds to the GitHub GraphQL schema, and contains the fields you're interested in querying. You can look up the GitHub GraphQL schema at https://developer.github.com/v4/query/. For example, to make the following GraphQL query: @@ -89,7 +89,7 @@ fmt.Println("CreatedAt:", query.Viewer.CreatedAt) ### Scalar Types -For each scalar in the GitHub GraphQL schema listed at https://developer.github.com/v4/reference/scalar/, there is a corresponding Go type in package `githubv4`. +For each scalar in the GitHub GraphQL schema listed at https://developer.github.com/v4/scalar/, there is a corresponding Go type in package `githubv4`. You can use these types when writing queries: @@ -127,7 +127,7 @@ var query struct { // Call client.Query() and use results in query... ``` -The [`DateTime`](https://developer.github.com/v4/reference/scalar/datetime/) scalar is described as "an ISO-8601 encoded UTC date string". If you wanted to fetch in that form without parsing it into a `time.Time`, you can use the `string` type. For example, this would work: +The [`DateTime`](https://developer.github.com/v4/scalar/datetime/) scalar is described as "an ISO-8601 encoded UTC date string". If you wanted to fetch in that form without parsing it into a `time.Time`, you can use the `string` type. For example, this would work: ```Go // import "html/template" @@ -336,7 +336,7 @@ for { } ``` -There is more than one way to perform pagination. Consider additional fields inside [`PageInfo`](https://developer.github.com/v4/reference/object/pageinfo/) object. +There is more than one way to perform pagination. Consider additional fields inside [`PageInfo`](https://developer.github.com/v4/object/pageinfo/) object. ### Mutations diff --git a/vendor/github.com/shurcooL/githubv4/enum.go b/vendor/github.com/shurcooL/githubv4/enum.go index 8293a23bb55d526a4558f62e0e5967cbf1b44e80..0ab9280082c7f40b3a694c79ad950137d68ecc47 100644 --- a/vendor/github.com/shurcooL/githubv4/enum.go +++ b/vendor/github.com/shurcooL/githubv4/enum.go @@ -36,6 +36,24 @@ const ( CommentCannotUpdateReasonLoginRequired CommentCannotUpdateReason = "LOGIN_REQUIRED" // You must be logged in to update this comment. CommentCannotUpdateReasonMaintenance CommentCannotUpdateReason = "MAINTENANCE" // Repository is under maintenance. CommentCannotUpdateReasonVerifiedEmailRequired CommentCannotUpdateReason = "VERIFIED_EMAIL_REQUIRED" // At least one email address must be verified to update this comment. + CommentCannotUpdateReasonDenied CommentCannotUpdateReason = "DENIED" // You cannot update this comment. +) + +// CommitContributionOrderField represents properties by which commit contribution connections can be ordered. +type CommitContributionOrderField string + +// Properties by which commit contribution connections can be ordered. +const ( + CommitContributionOrderFieldOccurredAt CommitContributionOrderField = "OCCURRED_AT" // Order commit contributions by when they were made. + CommitContributionOrderFieldCommitCount CommitContributionOrderField = "COMMIT_COUNT" // Order commit contributions by how many commits they represent. +) + +// ContributionOrderField represents properties by which contribution connections can be ordered. +type ContributionOrderField string + +// Properties by which contribution connections can be ordered. +const ( + ContributionOrderFieldOccurredAt ContributionOrderField = "OCCURRED_AT" // Order contributions by when they were made. ) // DefaultRepositoryPermissionField represents the possible default permissions for repositories. @@ -49,18 +67,28 @@ const ( DefaultRepositoryPermissionFieldAdmin DefaultRepositoryPermissionField = "ADMIN" // Can read, write, and administrate repos by default. ) +// DeploymentOrderField represents properties by which deployment connections can be ordered. +type DeploymentOrderField string + +// Properties by which deployment connections can be ordered. +const ( + DeploymentOrderFieldCreatedAt DeploymentOrderField = "CREATED_AT" // Order collection by creation time. +) + // DeploymentState represents the possible states in which a deployment can be. type DeploymentState string // The possible states in which a deployment can be. const ( - DeploymentStateAbandoned DeploymentState = "ABANDONED" // The pending deployment was not updated after 30 minutes. - DeploymentStateActive DeploymentState = "ACTIVE" // The deployment is currently active. - DeploymentStateDestroyed DeploymentState = "DESTROYED" // An inactive transient deployment. - DeploymentStateError DeploymentState = "ERROR" // The deployment experienced an error. - DeploymentStateFailure DeploymentState = "FAILURE" // The deployment has failed. - DeploymentStateInactive DeploymentState = "INACTIVE" // The deployment is inactive. - DeploymentStatePending DeploymentState = "PENDING" // The deployment is pending. + DeploymentStateAbandoned DeploymentState = "ABANDONED" // The pending deployment was not updated after 30 minutes. + DeploymentStateActive DeploymentState = "ACTIVE" // The deployment is currently active. + DeploymentStateDestroyed DeploymentState = "DESTROYED" // An inactive transient deployment. + DeploymentStateError DeploymentState = "ERROR" // The deployment experienced an error. + DeploymentStateFailure DeploymentState = "FAILURE" // The deployment has failed. + DeploymentStateInactive DeploymentState = "INACTIVE" // The deployment is inactive. + DeploymentStatePending DeploymentState = "PENDING" // The deployment is pending. + DeploymentStateQueued DeploymentState = "QUEUED" // The deployment has queued. + DeploymentStateInProgress DeploymentState = "IN_PROGRESS" // The deployment is in progress. ) // DeploymentStatusState represents the possible states for a deployment status. @@ -68,11 +96,13 @@ type DeploymentStatusState string // The possible states for a deployment status. const ( - DeploymentStatusStatePending DeploymentStatusState = "PENDING" // The deployment is pending. - DeploymentStatusStateSuccess DeploymentStatusState = "SUCCESS" // The deployment was successful. - DeploymentStatusStateFailure DeploymentStatusState = "FAILURE" // The deployment has failed. - DeploymentStatusStateInactive DeploymentStatusState = "INACTIVE" // The deployment is inactive. - DeploymentStatusStateError DeploymentStatusState = "ERROR" // The deployment experienced an error. + DeploymentStatusStatePending DeploymentStatusState = "PENDING" // The deployment is pending. + DeploymentStatusStateSuccess DeploymentStatusState = "SUCCESS" // The deployment was successful. + DeploymentStatusStateFailure DeploymentStatusState = "FAILURE" // The deployment has failed. + DeploymentStatusStateInactive DeploymentStatusState = "INACTIVE" // The deployment is inactive. + DeploymentStatusStateError DeploymentStatusState = "ERROR" // The deployment experienced an error. + DeploymentStatusStateQueued DeploymentStatusState = "QUEUED" // The deployment is queued. + DeploymentStatusStateInProgress DeploymentStatusState = "IN_PROGRESS" // The deployment is in progress. ) // GistOrderField represents properties by which gist connections can be ordered. @@ -115,9 +145,20 @@ const ( GitSignatureStateExpiredKey GitSignatureState = "EXPIRED_KEY" // Signing key expired. GitSignatureStateOcspPending GitSignatureState = "OCSP_PENDING" // Valid signature, pending certificate revocation checking. GitSignatureStateOcspError GitSignatureState = "OCSP_ERROR" // Valid siganture, though certificate revocation check failed. + GitSignatureStateBadCert GitSignatureState = "BAD_CERT" // The signing certificate or its chain could not be verified. GitSignatureStateOcspRevoked GitSignatureState = "OCSP_REVOKED" // One or more certificates in chain has been revoked. ) +// IdentityProviderConfigurationState represents the possible states in which authentication can be configured with an identity provider. +type IdentityProviderConfigurationState string + +// The possible states in which authentication can be configured with an identity provider. +const ( + IdentityProviderConfigurationStateEnforced IdentityProviderConfigurationState = "ENFORCED" // Authentication with an identity provider is configured and enforced. + IdentityProviderConfigurationStateConfigured IdentityProviderConfigurationState = "CONFIGURED" // Authentication with an identity provider is configured but not enforced. + IdentityProviderConfigurationStateUnconfigured IdentityProviderConfigurationState = "UNCONFIGURED" // Authentication with an identity provider is not configured. +) + // IssueOrderField represents properties by which issue connections can be ordered. type IssueOrderField string @@ -136,6 +177,7 @@ const ( IssuePubSubTopicUpdated IssuePubSubTopic = "UPDATED" // The channel ID for observing issue updates. IssuePubSubTopicMarkasread IssuePubSubTopic = "MARKASREAD" // The channel ID for marking an issue as read. IssuePubSubTopicTimeline IssuePubSubTopic = "TIMELINE" // The channel ID for updating items on the issue timeline. + IssuePubSubTopicState IssuePubSubTopic = "STATE" // The channel ID for observing issue state updates. ) // IssueState represents the possible states of an issue. @@ -147,6 +189,39 @@ const ( IssueStateClosed IssueState = "CLOSED" // An issue that has been closed. ) +// IssueTimelineItemsItemType represents the possible item types found in a timeline. +type IssueTimelineItemsItemType string + +// The possible item types found in a timeline. +const ( + IssueTimelineItemsItemTypeIssueComment IssueTimelineItemsItemType = "ISSUE_COMMENT" // Represents a comment on an Issue. + IssueTimelineItemsItemTypeCrossReferencedEvent IssueTimelineItemsItemType = "CROSS_REFERENCED_EVENT" // Represents a mention made by one issue or pull request to another. + IssueTimelineItemsItemTypeAddedToProjectEvent IssueTimelineItemsItemType = "ADDED_TO_PROJECT_EVENT" // Represents a 'added_to_project' event on a given issue or pull request. + IssueTimelineItemsItemTypeAssignedEvent IssueTimelineItemsItemType = "ASSIGNED_EVENT" // Represents an 'assigned' event on any assignable object. + IssueTimelineItemsItemTypeClosedEvent IssueTimelineItemsItemType = "CLOSED_EVENT" // Represents a 'closed' event on any `Closable`. + IssueTimelineItemsItemTypeCommentDeletedEvent IssueTimelineItemsItemType = "COMMENT_DELETED_EVENT" // Represents a 'comment_deleted' event on a given issue or pull request. + IssueTimelineItemsItemTypeConvertedNoteToIssueEvent IssueTimelineItemsItemType = "CONVERTED_NOTE_TO_ISSUE_EVENT" // Represents a 'converted_note_to_issue' event on a given issue or pull request. + IssueTimelineItemsItemTypeDemilestonedEvent IssueTimelineItemsItemType = "DEMILESTONED_EVENT" // Represents a 'demilestoned' event on a given issue or pull request. + IssueTimelineItemsItemTypeLabeledEvent IssueTimelineItemsItemType = "LABELED_EVENT" // Represents a 'labeled' event on a given issue or pull request. + IssueTimelineItemsItemTypeLockedEvent IssueTimelineItemsItemType = "LOCKED_EVENT" // Represents a 'locked' event on a given issue or pull request. + IssueTimelineItemsItemTypeMentionedEvent IssueTimelineItemsItemType = "MENTIONED_EVENT" // Represents a 'mentioned' event on a given issue or pull request. + IssueTimelineItemsItemTypeMilestonedEvent IssueTimelineItemsItemType = "MILESTONED_EVENT" // Represents a 'milestoned' event on a given issue or pull request. + IssueTimelineItemsItemTypeMovedColumnsInProjectEvent IssueTimelineItemsItemType = "MOVED_COLUMNS_IN_PROJECT_EVENT" // Represents a 'moved_columns_in_project' event on a given issue or pull request. + IssueTimelineItemsItemTypePinnedEvent IssueTimelineItemsItemType = "PINNED_EVENT" // Represents a 'pinned' event on a given issue or pull request. + IssueTimelineItemsItemTypeReferencedEvent IssueTimelineItemsItemType = "REFERENCED_EVENT" // Represents a 'referenced' event on a given `ReferencedSubject`. + IssueTimelineItemsItemTypeRemovedFromProjectEvent IssueTimelineItemsItemType = "REMOVED_FROM_PROJECT_EVENT" // Represents a 'removed_from_project' event on a given issue or pull request. + IssueTimelineItemsItemTypeRenamedTitleEvent IssueTimelineItemsItemType = "RENAMED_TITLE_EVENT" // Represents a 'renamed' event on a given issue or pull request. + IssueTimelineItemsItemTypeReopenedEvent IssueTimelineItemsItemType = "REOPENED_EVENT" // Represents a 'reopened' event on any `Closable`. + IssueTimelineItemsItemTypeSubscribedEvent IssueTimelineItemsItemType = "SUBSCRIBED_EVENT" // Represents a 'subscribed' event on a given `Subscribable`. + IssueTimelineItemsItemTypeTransferredEvent IssueTimelineItemsItemType = "TRANSFERRED_EVENT" // Represents a 'transferred' event on a given issue or pull request. + IssueTimelineItemsItemTypeUnassignedEvent IssueTimelineItemsItemType = "UNASSIGNED_EVENT" // Represents an 'unassigned' event on any assignable object. + IssueTimelineItemsItemTypeUnlabeledEvent IssueTimelineItemsItemType = "UNLABELED_EVENT" // Represents an 'unlabeled' event on a given issue or pull request. + IssueTimelineItemsItemTypeUnlockedEvent IssueTimelineItemsItemType = "UNLOCKED_EVENT" // Represents an 'unlocked' event on a given issue or pull request. + IssueTimelineItemsItemTypeUserBlockedEvent IssueTimelineItemsItemType = "USER_BLOCKED_EVENT" // Represents a 'user_blocked' event on a given user. + IssueTimelineItemsItemTypeUnpinnedEvent IssueTimelineItemsItemType = "UNPINNED_EVENT" // Represents an 'unpinned' event on a given issue or pull request. + IssueTimelineItemsItemTypeUnsubscribedEvent IssueTimelineItemsItemType = "UNSUBSCRIBED_EVENT" // Represents an 'unsubscribed' event on a given `Subscribable`. +) + // LanguageOrderField represents properties by which language connections can be ordered. type LanguageOrderField string @@ -225,6 +300,34 @@ const ( OrganizationInvitationTypeEmail OrganizationInvitationType = "EMAIL" // The invitation was to an email address. ) +// OrganizationMemberRole represents the possible roles within an organization for its members. +type OrganizationMemberRole string + +// The possible roles within an organization for its members. +const ( + OrganizationMemberRoleMember OrganizationMemberRole = "MEMBER" // The user is a member of the organization. + OrganizationMemberRoleAdmin OrganizationMemberRole = "ADMIN" // The user is an administrator of the organization. +) + +// PinnableItemType represents represents items that can be pinned to a profile page or dashboard. +type PinnableItemType string + +// Represents items that can be pinned to a profile page or dashboard. +const ( + PinnableItemTypeRepository PinnableItemType = "REPOSITORY" // A repository. + PinnableItemTypeGist PinnableItemType = "GIST" // A gist. + PinnableItemTypeIssue PinnableItemType = "ISSUE" // An issue. +) + +// ProjectCardArchivedState represents the possible archived states of a project card. +type ProjectCardArchivedState string + +// The possible archived states of a project card. +const ( + ProjectCardArchivedStateArchived ProjectCardArchivedState = "ARCHIVED" // A project card that is archived. + ProjectCardArchivedStateNotArchived ProjectCardArchivedState = "NOT_ARCHIVED" // A project card that is not archived. +) + // ProjectCardState represents various content states of a ProjectCard. type ProjectCardState string @@ -235,6 +338,16 @@ const ( ProjectCardStateRedacted ProjectCardState = "REDACTED" // The card is redacted. ) +// ProjectColumnPurpose represents the semantic purpose of the column - todo, in progress, or done. +type ProjectColumnPurpose string + +// The semantic purpose of the column - todo, in progress, or done. +const ( + ProjectColumnPurposeTodo ProjectColumnPurpose = "TODO" // The column contains cards still to be worked on. + ProjectColumnPurposeInProgress ProjectColumnPurpose = "IN_PROGRESS" // The column contains cards which are currently being worked on. + ProjectColumnPurposeDone ProjectColumnPurpose = "DONE" // The column contains cards which are complete. +) + // ProjectOrderField represents properties by which project connections can be ordered. type ProjectOrderField string @@ -254,6 +367,15 @@ const ( ProjectStateClosed ProjectState = "CLOSED" // The project is closed. ) +// PullRequestOrderField represents properties by which pull_requests connections can be ordered. +type PullRequestOrderField string + +// Properties by which pull_requests connections can be ordered. +const ( + PullRequestOrderFieldCreatedAt PullRequestOrderField = "CREATED_AT" // Order pull_requests by creation time. + PullRequestOrderFieldUpdatedAt PullRequestOrderField = "UPDATED_AT" // Order pull_requests by update time. +) + // PullRequestPubSubTopic represents the possible PubSub channels for a pull request. type PullRequestPubSubTopic string @@ -263,6 +385,16 @@ const ( PullRequestPubSubTopicMarkasread PullRequestPubSubTopic = "MARKASREAD" // The channel ID for marking an pull request as read. PullRequestPubSubTopicHeadRef PullRequestPubSubTopic = "HEAD_REF" // The channel ID for observing head ref updates. PullRequestPubSubTopicTimeline PullRequestPubSubTopic = "TIMELINE" // The channel ID for updating items on the pull request timeline. + PullRequestPubSubTopicState PullRequestPubSubTopic = "STATE" // The channel ID for observing pull request state updates. +) + +// PullRequestReviewCommentState represents the possible states of a pull request review comment. +type PullRequestReviewCommentState string + +// The possible states of a pull request review comment. +const ( + PullRequestReviewCommentStatePending PullRequestReviewCommentState = "PENDING" // A comment that is part of a pending review. + PullRequestReviewCommentStateSubmitted PullRequestReviewCommentState = "SUBMITTED" // A comment that is part of a submitted review. ) // PullRequestReviewEvent represents the possible events to perform on a pull request review. @@ -298,6 +430,55 @@ const ( PullRequestStateMerged PullRequestState = "MERGED" // A pull request that has been closed by being merged. ) +// PullRequestTimelineItemsItemType represents the possible item types found in a timeline. +type PullRequestTimelineItemsItemType string + +// The possible item types found in a timeline. +const ( + PullRequestTimelineItemsItemTypePullRequestCommit PullRequestTimelineItemsItemType = "PULL_REQUEST_COMMIT" // Represents a Git commit part of a pull request. + PullRequestTimelineItemsItemTypePullRequestCommitCommentThread PullRequestTimelineItemsItemType = "PULL_REQUEST_COMMIT_COMMENT_THREAD" // Represents a commit comment thread part of a pull request. + PullRequestTimelineItemsItemTypePullRequestReview PullRequestTimelineItemsItemType = "PULL_REQUEST_REVIEW" // A review object for a given pull request. + PullRequestTimelineItemsItemTypePullRequestReviewThread PullRequestTimelineItemsItemType = "PULL_REQUEST_REVIEW_THREAD" // A threaded list of comments for a given pull request. + PullRequestTimelineItemsItemTypePullRequestRevisionMarker PullRequestTimelineItemsItemType = "PULL_REQUEST_REVISION_MARKER" // Represents the latest point in the pull request timeline for which the viewer has seen the pull request's commits. + PullRequestTimelineItemsItemTypeBaseRefChangedEvent PullRequestTimelineItemsItemType = "BASE_REF_CHANGED_EVENT" // Represents a 'base_ref_changed' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeBaseRefForcePushedEvent PullRequestTimelineItemsItemType = "BASE_REF_FORCE_PUSHED_EVENT" // Represents a 'base_ref_force_pushed' event on a given pull request. + PullRequestTimelineItemsItemTypeDeployedEvent PullRequestTimelineItemsItemType = "DEPLOYED_EVENT" // Represents a 'deployed' event on a given pull request. + PullRequestTimelineItemsItemTypeDeploymentEnvironmentChangedEvent PullRequestTimelineItemsItemType = "DEPLOYMENT_ENVIRONMENT_CHANGED_EVENT" // Represents a 'deployment_environment_changed' event on a given pull request. + PullRequestTimelineItemsItemTypeHeadRefDeletedEvent PullRequestTimelineItemsItemType = "HEAD_REF_DELETED_EVENT" // Represents a 'head_ref_deleted' event on a given pull request. + PullRequestTimelineItemsItemTypeHeadRefForcePushedEvent PullRequestTimelineItemsItemType = "HEAD_REF_FORCE_PUSHED_EVENT" // Represents a 'head_ref_force_pushed' event on a given pull request. + PullRequestTimelineItemsItemTypeHeadRefRestoredEvent PullRequestTimelineItemsItemType = "HEAD_REF_RESTORED_EVENT" // Represents a 'head_ref_restored' event on a given pull request. + PullRequestTimelineItemsItemTypeMergedEvent PullRequestTimelineItemsItemType = "MERGED_EVENT" // Represents a 'merged' event on a given pull request. + PullRequestTimelineItemsItemTypeReviewDismissedEvent PullRequestTimelineItemsItemType = "REVIEW_DISMISSED_EVENT" // Represents a 'review_dismissed' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeReviewRequestedEvent PullRequestTimelineItemsItemType = "REVIEW_REQUESTED_EVENT" // Represents an 'review_requested' event on a given pull request. + PullRequestTimelineItemsItemTypeReviewRequestRemovedEvent PullRequestTimelineItemsItemType = "REVIEW_REQUEST_REMOVED_EVENT" // Represents an 'review_request_removed' event on a given pull request. + PullRequestTimelineItemsItemTypeIssueComment PullRequestTimelineItemsItemType = "ISSUE_COMMENT" // Represents a comment on an Issue. + PullRequestTimelineItemsItemTypeCrossReferencedEvent PullRequestTimelineItemsItemType = "CROSS_REFERENCED_EVENT" // Represents a mention made by one issue or pull request to another. + PullRequestTimelineItemsItemTypeAddedToProjectEvent PullRequestTimelineItemsItemType = "ADDED_TO_PROJECT_EVENT" // Represents a 'added_to_project' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeAssignedEvent PullRequestTimelineItemsItemType = "ASSIGNED_EVENT" // Represents an 'assigned' event on any assignable object. + PullRequestTimelineItemsItemTypeClosedEvent PullRequestTimelineItemsItemType = "CLOSED_EVENT" // Represents a 'closed' event on any `Closable`. + PullRequestTimelineItemsItemTypeCommentDeletedEvent PullRequestTimelineItemsItemType = "COMMENT_DELETED_EVENT" // Represents a 'comment_deleted' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeConvertedNoteToIssueEvent PullRequestTimelineItemsItemType = "CONVERTED_NOTE_TO_ISSUE_EVENT" // Represents a 'converted_note_to_issue' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeDemilestonedEvent PullRequestTimelineItemsItemType = "DEMILESTONED_EVENT" // Represents a 'demilestoned' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeLabeledEvent PullRequestTimelineItemsItemType = "LABELED_EVENT" // Represents a 'labeled' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeLockedEvent PullRequestTimelineItemsItemType = "LOCKED_EVENT" // Represents a 'locked' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeMentionedEvent PullRequestTimelineItemsItemType = "MENTIONED_EVENT" // Represents a 'mentioned' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeMilestonedEvent PullRequestTimelineItemsItemType = "MILESTONED_EVENT" // Represents a 'milestoned' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeMovedColumnsInProjectEvent PullRequestTimelineItemsItemType = "MOVED_COLUMNS_IN_PROJECT_EVENT" // Represents a 'moved_columns_in_project' event on a given issue or pull request. + PullRequestTimelineItemsItemTypePinnedEvent PullRequestTimelineItemsItemType = "PINNED_EVENT" // Represents a 'pinned' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeReferencedEvent PullRequestTimelineItemsItemType = "REFERENCED_EVENT" // Represents a 'referenced' event on a given `ReferencedSubject`. + PullRequestTimelineItemsItemTypeRemovedFromProjectEvent PullRequestTimelineItemsItemType = "REMOVED_FROM_PROJECT_EVENT" // Represents a 'removed_from_project' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeRenamedTitleEvent PullRequestTimelineItemsItemType = "RENAMED_TITLE_EVENT" // Represents a 'renamed' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeReopenedEvent PullRequestTimelineItemsItemType = "REOPENED_EVENT" // Represents a 'reopened' event on any `Closable`. + PullRequestTimelineItemsItemTypeSubscribedEvent PullRequestTimelineItemsItemType = "SUBSCRIBED_EVENT" // Represents a 'subscribed' event on a given `Subscribable`. + PullRequestTimelineItemsItemTypeTransferredEvent PullRequestTimelineItemsItemType = "TRANSFERRED_EVENT" // Represents a 'transferred' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeUnassignedEvent PullRequestTimelineItemsItemType = "UNASSIGNED_EVENT" // Represents an 'unassigned' event on any assignable object. + PullRequestTimelineItemsItemTypeUnlabeledEvent PullRequestTimelineItemsItemType = "UNLABELED_EVENT" // Represents an 'unlabeled' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeUnlockedEvent PullRequestTimelineItemsItemType = "UNLOCKED_EVENT" // Represents an 'unlocked' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeUserBlockedEvent PullRequestTimelineItemsItemType = "USER_BLOCKED_EVENT" // Represents a 'user_blocked' event on a given user. + PullRequestTimelineItemsItemTypeUnpinnedEvent PullRequestTimelineItemsItemType = "UNPINNED_EVENT" // Represents an 'unpinned' event on a given issue or pull request. + PullRequestTimelineItemsItemTypeUnsubscribedEvent PullRequestTimelineItemsItemType = "UNSUBSCRIBED_EVENT" // Represents an 'unsubscribed' event on a given `Subscribable`. +) + // ReactionContent represents emojis that can be attached to Issues, Pull Requests and Comments. type ReactionContent string @@ -309,6 +490,8 @@ const ( ReactionContentHooray ReactionContent = "HOORAY" // Represents the 🎉 emoji. ReactionContentConfused ReactionContent = "CONFUSED" // Represents the 😕 emoji. ReactionContentHeart ReactionContent = "HEART" // Represents the ❤️ emoji. + ReactionContentRocket ReactionContent = "ROCKET" // Represents the 🚀 emoji. + ReactionContentEyes ReactionContent = "EYES" // Represents the 👀 emoji. ) // ReactionOrderField represents a list of fields that reactions can be ordered by. @@ -337,6 +520,18 @@ const ( ReleaseOrderFieldName ReleaseOrderField = "NAME" // Order releases alphabetically by name. ) +// ReportedContentClassifiers represents the reasons a piece of content can be reported or minimized. +type ReportedContentClassifiers string + +// The reasons a piece of content can be reported or minimized. +const ( + ReportedContentClassifiersSpam ReportedContentClassifiers = "SPAM" // A spammy piece of content. + ReportedContentClassifiersAbuse ReportedContentClassifiers = "ABUSE" // An abusive or harassing piece of content. + ReportedContentClassifiersOffTopic ReportedContentClassifiers = "OFF_TOPIC" // An irrelevant piece of content. + ReportedContentClassifiersOutdated ReportedContentClassifiers = "OUTDATED" // An outdated piece of content. + ReportedContentClassifiersResolved ReportedContentClassifiers = "RESOLVED" // The content has been resolved. +) + // RepositoryAffiliation represents the affiliation of a user to a repository. type RepositoryAffiliation string @@ -396,9 +591,11 @@ type RepositoryPermission string // The access level to a repository. const ( - RepositoryPermissionAdmin RepositoryPermission = "ADMIN" // Can read, clone, push, and add collaborators. - RepositoryPermissionWrite RepositoryPermission = "WRITE" // Can read, clone and push. - RepositoryPermissionRead RepositoryPermission = "READ" // Can read and clone. + RepositoryPermissionAdmin RepositoryPermission = "ADMIN" // Can read, clone, and push to this repository. Can also manage issues, pull requests, and repository settings, including adding collaborators. + RepositoryPermissionMaintain RepositoryPermission = "MAINTAIN" // Can read, clone, and push to this repository. They can also manage issues, pull requests, and some repository settings. + RepositoryPermissionWrite RepositoryPermission = "WRITE" // Can read, clone, and push to this repository. Can also manage issues and pull requests. + RepositoryPermissionTriage RepositoryPermission = "TRIAGE" // Can read and clone this repository. Can also manage issues and pull requests. + RepositoryPermissionRead RepositoryPermission = "READ" // Can read and clone this repository. Can also open and comment on issues and pull requests. ) // RepositoryPrivacy represents the privacy of a repository. @@ -420,6 +617,55 @@ const ( SearchTypeUser SearchType = "USER" // Returns results matching users and organizations on GitHub. ) +// SecurityAdvisoryEcosystem represents the possible ecosystems of a security vulnerability's package. +type SecurityAdvisoryEcosystem string + +// The possible ecosystems of a security vulnerability's package. +const ( + SecurityAdvisoryEcosystemRubygems SecurityAdvisoryEcosystem = "RUBYGEMS" // Ruby gems hosted at RubyGems.org. + SecurityAdvisoryEcosystemNpm SecurityAdvisoryEcosystem = "NPM" // JavaScript packages hosted at npmjs.com. + SecurityAdvisoryEcosystemPip SecurityAdvisoryEcosystem = "PIP" // Python packages hosted at PyPI.org. + SecurityAdvisoryEcosystemMaven SecurityAdvisoryEcosystem = "MAVEN" // Java artifacts hosted at the Maven central repository. + SecurityAdvisoryEcosystemNuget SecurityAdvisoryEcosystem = "NUGET" // .NET packages hosted at the NuGet Gallery. +) + +// SecurityAdvisoryIdentifierType represents identifier formats available for advisories. +type SecurityAdvisoryIdentifierType string + +// Identifier formats available for advisories. +const ( + SecurityAdvisoryIdentifierTypeCve SecurityAdvisoryIdentifierType = "CVE" // Common Vulnerabilities and Exposures Identifier. + SecurityAdvisoryIdentifierTypeGhsa SecurityAdvisoryIdentifierType = "GHSA" // GitHub Security Advisory ID. +) + +// SecurityAdvisoryOrderField represents properties by which security advisory connections can be ordered. +type SecurityAdvisoryOrderField string + +// Properties by which security advisory connections can be ordered. +const ( + SecurityAdvisoryOrderFieldPublishedAt SecurityAdvisoryOrderField = "PUBLISHED_AT" // Order advisories by publication time. + SecurityAdvisoryOrderFieldUpdatedAt SecurityAdvisoryOrderField = "UPDATED_AT" // Order advisories by update time. +) + +// SecurityAdvisorySeverity represents severity of the vulnerability. +type SecurityAdvisorySeverity string + +// Severity of the vulnerability. +const ( + SecurityAdvisorySeverityLow SecurityAdvisorySeverity = "LOW" // Low. + SecurityAdvisorySeverityModerate SecurityAdvisorySeverity = "MODERATE" // Moderate. + SecurityAdvisorySeverityHigh SecurityAdvisorySeverity = "HIGH" // High. + SecurityAdvisorySeverityCritical SecurityAdvisorySeverity = "CRITICAL" // Critical. +) + +// SecurityVulnerabilityOrderField represents properties by which security vulnerability connections can be ordered. +type SecurityVulnerabilityOrderField string + +// Properties by which security vulnerability connections can be ordered. +const ( + SecurityVulnerabilityOrderFieldUpdatedAt SecurityVulnerabilityOrderField = "UPDATED_AT" // Order vulnerability by update time. +) + // StarOrderField represents properties by which star connections can be ordered. type StarOrderField string @@ -445,7 +691,7 @@ type SubscriptionState string // The possible states of a subscription. const ( - SubscriptionStateUnsubscribed SubscriptionState = "UNSUBSCRIBED" // The User is only notified when particpating or @mentioned. + SubscriptionStateUnsubscribed SubscriptionState = "UNSUBSCRIBED" // The User is only notified when participating or @mentioned. SubscriptionStateSubscribed SubscriptionState = "SUBSCRIBED" // The User is notified of all conversations. SubscriptionStateIgnored SubscriptionState = "IGNORED" // The User is never notified. ) @@ -527,3 +773,23 @@ const ( TopicSuggestionDeclineReasonPersonalPreference TopicSuggestionDeclineReason = "PERSONAL_PREFERENCE" // The viewer does not like the suggested topic. TopicSuggestionDeclineReasonTooGeneral TopicSuggestionDeclineReason = "TOO_GENERAL" // The suggested topic is too general for the repository. ) + +// UserBlockDuration represents the possible durations that a user can be blocked for. +type UserBlockDuration string + +// The possible durations that a user can be blocked for. +const ( + UserBlockDurationOneDay UserBlockDuration = "ONE_DAY" // The user was blocked for 1 day. + UserBlockDurationThreeDays UserBlockDuration = "THREE_DAYS" // The user was blocked for 3 days. + UserBlockDurationOneWeek UserBlockDuration = "ONE_WEEK" // The user was blocked for 7 days. + UserBlockDurationOneMonth UserBlockDuration = "ONE_MONTH" // The user was blocked for 30 days. + UserBlockDurationPermanent UserBlockDuration = "PERMANENT" // The user was blocked permanently. +) + +// UserStatusOrderField represents properties by which user status connections can be ordered. +type UserStatusOrderField string + +// Properties by which user status connections can be ordered. +const ( + UserStatusOrderFieldUpdatedAt UserStatusOrderField = "UPDATED_AT" // Order user statuses by when they were updated. +) diff --git a/vendor/github.com/shurcooL/githubv4/gen.go b/vendor/github.com/shurcooL/githubv4/gen.go index 43ad1e35ca08a91158eb9c0bd0881c11bd50ecc4..628ce57b82df7233579a54b35b7c2263b864bd39 100644 --- a/vendor/github.com/shurcooL/githubv4/gen.go +++ b/vendor/github.com/shurcooL/githubv4/gen.go @@ -86,12 +86,12 @@ package githubv4 {{- define "enum" -}} -// {{.name}} {{.description | endSentence}} +// {{.name}} {{.description | clean | endSentence}} type {{.name}} string -// {{.description | fullSentence}} +// {{.description | clean | fullSentence}} const ({{range .enumValues}} - {{$.name}}{{.name | enumIdentifier}} {{$.name}} = {{.name | quote}} // {{.description | fullSentence}}{{end}} + {{$.name}}{{.name | enumIdentifier}} {{$.name}} = {{.name | quote}} // {{.description | clean | fullSentence}}{{end}} ) {{- end -}} `), @@ -110,12 +110,12 @@ type Input interface{} {{- define "inputObject" -}} -// {{.name}} {{.description | endSentence}} +// {{.name}} {{.description | clean | endSentence}} type {{.name}} struct {{"{"}}{{range .inputFields}}{{if eq .type.kind "NON_NULL"}} - // {{.description | fullSentence}} (Required.) + // {{.description | clean | fullSentence}} (Required.) {{.name | identifier}} {{.type | type}} ` + "`" + `json:"{{.name}}"` + "`" + `{{end}}{{end}} {{range .inputFields}}{{if ne .type.kind "NON_NULL"}} - // {{.description | fullSentence}} (Optional.) + // {{.description | clean | fullSentence}} (Optional.) {{.name | identifier}} {{.type | type}} ` + "`" + `json:"{{.name}},omitempty"` + "`" + `{{end}}{{end}} } {{- end -}} @@ -167,6 +167,7 @@ func t(text string) *template.Template { "identifier": func(name string) string { return ident.ParseLowerCamelCase(name).ToMixedCaps() }, "enumIdentifier": func(name string) string { return ident.ParseScreamingSnakeCase(name).ToMixedCaps() }, "type": typeString, + "clean": func(s string) string { return strings.Join(strings.Fields(s), " ") }, "endSentence": func(s string) string { s = strings.ToLower(s[0:1]) + s[1:] switch { diff --git a/vendor/github.com/shurcooL/githubv4/input.go b/vendor/github.com/shurcooL/githubv4/input.go index ce1976f8e7203ddc44797793119740c9f581da36..da00e3b31f6690ecf2a8479e710c11b3e1b32705 100644 --- a/vendor/github.com/shurcooL/githubv4/input.go +++ b/vendor/github.com/shurcooL/githubv4/input.go @@ -4,7 +4,7 @@ package githubv4 // Input represents one of the Input structs: // -// AcceptTopicSuggestionInput, AddCommentInput, AddProjectCardInput, AddProjectColumnInput, AddPullRequestReviewCommentInput, AddPullRequestReviewInput, AddReactionInput, AddStarInput, CommitAuthor, CreateProjectInput, DeclineTopicSuggestionInput, DeleteProjectCardInput, DeleteProjectColumnInput, DeleteProjectInput, DeletePullRequestReviewInput, DismissPullRequestReviewInput, DraftPullRequestReviewComment, GistOrder, IssueOrder, LanguageOrder, LockLockableInput, MilestoneOrder, MoveProjectCardInput, MoveProjectColumnInput, ProjectOrder, ReactionOrder, RefOrder, ReleaseOrder, RemoveOutsideCollaboratorInput, RemoveReactionInput, RemoveStarInput, RepositoryOrder, RequestReviewsInput, StarOrder, SubmitPullRequestReviewInput, TeamMemberOrder, TeamOrder, TeamRepositoryOrder, UpdateProjectCardInput, UpdateProjectColumnInput, UpdateProjectInput, UpdatePullRequestReviewCommentInput, UpdatePullRequestReviewInput, UpdateSubscriptionInput, UpdateTopicsInput. +// AcceptTopicSuggestionInput, AddAssigneesToAssignableInput, AddCommentInput, AddLabelsToLabelableInput, AddProjectCardInput, AddProjectColumnInput, AddPullRequestReviewCommentInput, AddPullRequestReviewInput, AddReactionInput, AddStarInput, ChangeUserStatusInput, ClearLabelsFromLabelableInput, CloneProjectInput, CloseIssueInput, ClosePullRequestInput, CommitAuthor, CommitContributionOrder, ContributionOrder, ConvertProjectCardNoteToIssueInput, CreateBranchProtectionRuleInput, CreateContentAttachmentInput, CreateIssueInput, CreateProjectInput, CreatePullRequestInput, DeclineTopicSuggestionInput, DeleteBranchProtectionRuleInput, DeleteIssueCommentInput, DeleteIssueInput, DeleteProjectCardInput, DeleteProjectColumnInput, DeleteProjectInput, DeletePullRequestReviewCommentInput, DeletePullRequestReviewInput, DeploymentOrder, DismissPullRequestReviewInput, DraftPullRequestReviewComment, GistOrder, ImportProjectInput, IssueFilters, IssueOrder, LanguageOrder, LockLockableInput, MergePullRequestInput, MilestoneOrder, MinimizeCommentInput, MoveProjectCardInput, MoveProjectColumnInput, PinIssueInput, ProjectCardImport, ProjectColumnImport, ProjectOrder, PullRequestOrder, ReactionOrder, RefOrder, ReleaseOrder, RemoveAssigneesFromAssignableInput, RemoveLabelsFromLabelableInput, RemoveOutsideCollaboratorInput, RemoveReactionInput, RemoveStarInput, ReopenIssueInput, ReopenPullRequestInput, RepositoryOrder, RequestReviewsInput, ResolveReviewThreadInput, SecurityAdvisoryIdentifierFilter, SecurityAdvisoryOrder, SecurityVulnerabilityOrder, StarOrder, SubmitPullRequestReviewInput, TeamMemberOrder, TeamOrder, TeamRepositoryOrder, UnlockLockableInput, UnmarkIssueAsDuplicateInput, UnminimizeCommentInput, UnpinIssueInput, UnresolveReviewThreadInput, UpdateBranchProtectionRuleInput, UpdateIssueCommentInput, UpdateIssueInput, UpdateProjectCardInput, UpdateProjectColumnInput, UpdateProjectInput, UpdatePullRequestInput, UpdatePullRequestReviewCommentInput, UpdatePullRequestReviewInput, UpdateSubscriptionInput, UpdateTopicsInput, UserStatusOrder. type Input interface{} // AcceptTopicSuggestionInput is an autogenerated input type of AcceptTopicSuggestion. @@ -18,6 +18,17 @@ type AcceptTopicSuggestionInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// AddAssigneesToAssignableInput is an autogenerated input type of AddAssigneesToAssignable. +type AddAssigneesToAssignableInput struct { + // The id of the assignable object to add assignees to. (Required.) + AssignableID ID `json:"assignableId"` + // The id of users to add as assignees. (Required.) + AssigneeIDs []ID `json:"assigneeIds"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // AddCommentInput is an autogenerated input type of AddComment. type AddCommentInput struct { // The Node ID of the subject to modify. (Required.) @@ -29,6 +40,17 @@ type AddCommentInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// AddLabelsToLabelableInput is an autogenerated input type of AddLabelsToLabelable. +type AddLabelsToLabelableInput struct { + // The id of the labelable object to add labels to. (Required.) + LabelableID ID `json:"labelableId"` + // The ids of the labels to add. (Required.) + LabelIDs []ID `json:"labelIds"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // AddProjectCardInput is an autogenerated input type of AddProjectCard. type AddProjectCardInput struct { // The Node ID of the ProjectColumn. (Required.) @@ -109,6 +131,69 @@ type AddStarInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// ChangeUserStatusInput is an autogenerated input type of ChangeUserStatus. +type ChangeUserStatusInput struct { + + // The emoji to represent your status. Can either be a native Unicode emoji or an emoji name with colons, e.g., :grinning:. (Optional.) + Emoji *String `json:"emoji,omitempty"` + // A short description of your current status. (Optional.) + Message *String `json:"message,omitempty"` + // The ID of the organization whose members will be allowed to see the status. If omitted, the status will be publicly visible. (Optional.) + OrganizationID *ID `json:"organizationId,omitempty"` + // Whether this status should indicate you are not fully available on GitHub, e.g., you are away. (Optional.) + LimitedAvailability *Boolean `json:"limitedAvailability,omitempty"` + // If set, the user status will not be shown after this date. (Optional.) + ExpiresAt *DateTime `json:"expiresAt,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// ClearLabelsFromLabelableInput is an autogenerated input type of ClearLabelsFromLabelable. +type ClearLabelsFromLabelableInput struct { + // The id of the labelable object to clear the labels from. (Required.) + LabelableID ID `json:"labelableId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// CloneProjectInput is an autogenerated input type of CloneProject. +type CloneProjectInput struct { + // The owner ID to create the project under. (Required.) + TargetOwnerID ID `json:"targetOwnerId"` + // The source project to clone. (Required.) + SourceID ID `json:"sourceId"` + // Whether or not to clone the source project's workflows. (Required.) + IncludeWorkflows Boolean `json:"includeWorkflows"` + // The name of the project. (Required.) + Name String `json:"name"` + + // The description of the project. (Optional.) + Body *String `json:"body,omitempty"` + // The visibility of the project, defaults to false (private). (Optional.) + Public *Boolean `json:"public,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// CloseIssueInput is an autogenerated input type of CloseIssue. +type CloseIssueInput struct { + // ID of the issue to be closed. (Required.) + IssueID ID `json:"issueId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// ClosePullRequestInput is an autogenerated input type of ClosePullRequest. +type ClosePullRequestInput struct { + // ID of the pull request to be closed. (Required.) + PullRequestID ID `json:"pullRequestId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // CommitAuthor specifies an author for filtering Git commits. type CommitAuthor struct { @@ -118,6 +203,108 @@ type CommitAuthor struct { Emails *[]String `json:"emails,omitempty"` } +// CommitContributionOrder represents ordering options for commit contribution connections. +type CommitContributionOrder struct { + // The field by which to order commit contributions. (Required.) + Field CommitContributionOrderField `json:"field"` + // The ordering direction. (Required.) + Direction OrderDirection `json:"direction"` +} + +// ContributionOrder represents ordering options for contribution connections. +type ContributionOrder struct { + // The field by which to order contributions. (Required.) + Field ContributionOrderField `json:"field"` + // The ordering direction. (Required.) + Direction OrderDirection `json:"direction"` +} + +// ConvertProjectCardNoteToIssueInput is an autogenerated input type of ConvertProjectCardNoteToIssue. +type ConvertProjectCardNoteToIssueInput struct { + // The ProjectCard ID to convert. (Required.) + ProjectCardID ID `json:"projectCardId"` + // The ID of the repository to create the issue in. (Required.) + RepositoryID ID `json:"repositoryId"` + + // The title of the newly created issue. Defaults to the card's note text. (Optional.) + Title *String `json:"title,omitempty"` + // The body of the newly created issue. (Optional.) + Body *String `json:"body,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// CreateBranchProtectionRuleInput is an autogenerated input type of CreateBranchProtectionRule. +type CreateBranchProtectionRuleInput struct { + // The global relay id of the repository in which a new branch protection rule should be created in. (Required.) + RepositoryID ID `json:"repositoryId"` + // The glob-like pattern used to determine matching branches. (Required.) + Pattern String `json:"pattern"` + + // Are approving reviews required to update matching branches. (Optional.) + RequiresApprovingReviews *Boolean `json:"requiresApprovingReviews,omitempty"` + // Number of approving reviews required to update matching branches. (Optional.) + RequiredApprovingReviewCount *Int `json:"requiredApprovingReviewCount,omitempty"` + // Are commits required to be signed. (Optional.) + RequiresCommitSignatures *Boolean `json:"requiresCommitSignatures,omitempty"` + // Can admins overwrite branch protection. (Optional.) + IsAdminEnforced *Boolean `json:"isAdminEnforced,omitempty"` + // Are status checks required to update matching branches. (Optional.) + RequiresStatusChecks *Boolean `json:"requiresStatusChecks,omitempty"` + // Are branches required to be up to date before merging. (Optional.) + RequiresStrictStatusChecks *Boolean `json:"requiresStrictStatusChecks,omitempty"` + // Are reviews from code owners required to update matching branches. (Optional.) + RequiresCodeOwnerReviews *Boolean `json:"requiresCodeOwnerReviews,omitempty"` + // Will new commits pushed to matching branches dismiss pull request review approvals. (Optional.) + DismissesStaleReviews *Boolean `json:"dismissesStaleReviews,omitempty"` + // Is dismissal of pull request reviews restricted. (Optional.) + RestrictsReviewDismissals *Boolean `json:"restrictsReviewDismissals,omitempty"` + // A list of User or Team IDs allowed to dismiss reviews on pull requests targeting matching branches. (Optional.) + ReviewDismissalActorIDs *[]ID `json:"reviewDismissalActorIds,omitempty"` + // Is pushing to matching branches restricted. (Optional.) + RestrictsPushes *Boolean `json:"restrictsPushes,omitempty"` + // A list of User or Team IDs allowed to push to matching branches. (Optional.) + PushActorIDs *[]ID `json:"pushActorIds,omitempty"` + // List of required status check contexts that must pass for commits to be accepted to matching branches. (Optional.) + RequiredStatusCheckContexts *[]String `json:"requiredStatusCheckContexts,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// CreateContentAttachmentInput is an autogenerated input type of CreateContentAttachment. +type CreateContentAttachmentInput struct { + // The node ID of the content_reference. (Required.) + ContentReferenceID ID `json:"contentReferenceId"` + // The title of the content attachment. (Required.) + Title String `json:"title"` + // The body of the content attachment, which may contain markdown. (Required.) + Body String `json:"body"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// CreateIssueInput is an autogenerated input type of CreateIssue. +type CreateIssueInput struct { + // The Node ID of the repository. (Required.) + RepositoryID ID `json:"repositoryId"` + // The title for the issue. (Required.) + Title String `json:"title"` + + // The body for the issue description. (Optional.) + Body *String `json:"body,omitempty"` + // The Node ID for the user assignee for this issue. (Optional.) + AssigneeIDs *[]ID `json:"assigneeIds,omitempty"` + // The Node ID of the milestone for this issue. (Optional.) + MilestoneID *ID `json:"milestoneId,omitempty"` + // An array of Node IDs of labels for this issue. (Optional.) + LabelIDs *[]ID `json:"labelIds,omitempty"` + // An array of Node IDs for projects associated with this issue. (Optional.) + ProjectIDs *[]ID `json:"projectIds,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // CreateProjectInput is an autogenerated input type of CreateProject. type CreateProjectInput struct { // The owner ID to create the project under. (Required.) @@ -131,6 +318,25 @@ type CreateProjectInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// CreatePullRequestInput is an autogenerated input type of CreatePullRequest. +type CreatePullRequestInput struct { + // The Node ID of the repository. (Required.) + RepositoryID ID `json:"repositoryId"` + // The name of the branch you want your changes pulled into. This should be an existing branch on the current repository. You cannot update the base branch on a pull request to point to another repository. (Required.) + BaseRefName String `json:"baseRefName"` + // The name of the branch where your changes are implemented. For cross-repository pull requests in the same network, namespace `head_ref_name` with a user like this: `username:branch`. (Required.) + HeadRefName String `json:"headRefName"` + // The title of the pull request. (Required.) + Title String `json:"title"` + + // The contents of the pull request. (Optional.) + Body *String `json:"body,omitempty"` + // Indicates whether maintainers can modify the pull request. (Optional.) + MaintainerCanModify *Boolean `json:"maintainerCanModify,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // DeclineTopicSuggestionInput is an autogenerated input type of DeclineTopicSuggestion. type DeclineTopicSuggestionInput struct { // The Node ID of the repository. (Required.) @@ -144,6 +350,33 @@ type DeclineTopicSuggestionInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// DeleteBranchProtectionRuleInput is an autogenerated input type of DeleteBranchProtectionRule. +type DeleteBranchProtectionRuleInput struct { + // The global relay id of the branch protection rule to be deleted. (Required.) + BranchProtectionRuleID ID `json:"branchProtectionRuleId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// DeleteIssueCommentInput is an autogenerated input type of DeleteIssueComment. +type DeleteIssueCommentInput struct { + // The ID of the comment to delete. (Required.) + ID ID `json:"id"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// DeleteIssueInput is an autogenerated input type of DeleteIssue. +type DeleteIssueInput struct { + // The ID of the issue to delete. (Required.) + IssueID ID `json:"issueId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // DeleteProjectCardInput is an autogenerated input type of DeleteProjectCard. type DeleteProjectCardInput struct { // The id of the card to delete. (Required.) @@ -171,6 +404,15 @@ type DeleteProjectInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// DeletePullRequestReviewCommentInput is an autogenerated input type of DeletePullRequestReviewComment. +type DeletePullRequestReviewCommentInput struct { + // The ID of the comment to delete. (Required.) + ID ID `json:"id"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // DeletePullRequestReviewInput is an autogenerated input type of DeletePullRequestReview. type DeletePullRequestReviewInput struct { // The Node ID of the pull request review to delete. (Required.) @@ -180,6 +422,14 @@ type DeletePullRequestReviewInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// DeploymentOrder represents ordering options for deployment connections. +type DeploymentOrder struct { + // The field to order deployments by. (Required.) + Field DeploymentOrderField `json:"field"` + // The ordering direction. (Required.) + Direction OrderDirection `json:"direction"` +} + // DismissPullRequestReviewInput is an autogenerated input type of DismissPullRequestReview. type DismissPullRequestReviewInput struct { // The Node ID of the pull request review to modify. (Required.) @@ -209,6 +459,44 @@ type GistOrder struct { Direction OrderDirection `json:"direction"` } +// ImportProjectInput is an autogenerated input type of ImportProject. +type ImportProjectInput struct { + // The name of the Organization or User to create the Project under. (Required.) + OwnerName String `json:"ownerName"` + // The name of Project. (Required.) + Name String `json:"name"` + // A list of columns containing issues and pull requests. (Required.) + ColumnImports []ProjectColumnImport `json:"columnImports"` + + // The description of Project. (Optional.) + Body *String `json:"body,omitempty"` + // Whether the Project is public or not. (Optional.) + Public *Boolean `json:"public,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// IssueFilters represents ways in which to filter lists of issues. +type IssueFilters struct { + + // List issues assigned to given name. Pass in `null` for issues with no assigned user, and `*` for issues assigned to any user. (Optional.) + Assignee *String `json:"assignee,omitempty"` + // List issues created by given name. (Optional.) + CreatedBy *String `json:"createdBy,omitempty"` + // List issues where the list of label names exist on the issue. (Optional.) + Labels *[]String `json:"labels,omitempty"` + // List issues where the given name is mentioned in the issue. (Optional.) + Mentioned *String `json:"mentioned,omitempty"` + // List issues by given milestone argument. If an string representation of an integer is passed, it should refer to a milestone by its number field. Pass in `null` for issues with no milestone, and `*` for issues that are assigned to any milestone. (Optional.) + Milestone *String `json:"milestone,omitempty"` + // List issues that have been updated at or after the given date. (Optional.) + Since *DateTime `json:"since,omitempty"` + // List issues filtered by the list of states given. (Optional.) + States *[]IssueState `json:"states,omitempty"` + // List issues subscribed to by viewer. (Optional.) + ViewerSubscribed *Boolean `json:"viewerSubscribed,omitempty"` +} + // IssueOrder represents ways in which lists of issues can be ordered upon return. type IssueOrder struct { // The field in which to order issues by. (Required.) @@ -236,6 +524,21 @@ type LockLockableInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// MergePullRequestInput is an autogenerated input type of MergePullRequest. +type MergePullRequestInput struct { + // ID of the pull request to be merged. (Required.) + PullRequestID ID `json:"pullRequestId"` + + // Commit headline to use for the merge commit; if omitted, a default message will be used. (Optional.) + CommitHeadline *String `json:"commitHeadline,omitempty"` + // Commit body to use for the merge commit; if omitted, a default message will be used. (Optional.) + CommitBody *String `json:"commitBody,omitempty"` + // OID that the pull request head ref must match to allow merge; if omitted, no check is performed. (Optional.) + ExpectedHeadOid *GitObjectID `json:"expectedHeadOid,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // MilestoneOrder represents ordering options for milestone connections. type MilestoneOrder struct { // The field to order milestones by. (Required.) @@ -244,6 +547,17 @@ type MilestoneOrder struct { Direction OrderDirection `json:"direction"` } +// MinimizeCommentInput is an autogenerated input type of MinimizeComment. +type MinimizeCommentInput struct { + // The Node ID of the subject to modify. (Required.) + SubjectID ID `json:"subjectId"` + // The classification of comment. (Required.) + Classifier ReportedContentClassifiers `json:"classifier"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // MoveProjectCardInput is an autogenerated input type of MoveProjectCard. type MoveProjectCardInput struct { // The id of the card to move. (Required.) @@ -268,6 +582,34 @@ type MoveProjectColumnInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// PinIssueInput is an autogenerated input type of PinIssue. +type PinIssueInput struct { + // The ID of the issue to be pinned. (Required.) + IssueID ID `json:"issueId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// ProjectCardImport represents an issue or PR and its owning repository to be used in a project card. +type ProjectCardImport struct { + // Repository name with owner (owner/repository). (Required.) + Repository String `json:"repository"` + // The issue or pull request number. (Required.) + Number Int `json:"number"` +} + +// ProjectColumnImport represents a project column and a list of its issues and PRs. +type ProjectColumnImport struct { + // The name of the column. (Required.) + ColumnName String `json:"columnName"` + // The position of the column, starting from 0. (Required.) + Position Int `json:"position"` + + // A list of issues and pull requests in the column. (Optional.) + Issues *[]ProjectCardImport `json:"issues,omitempty"` +} + // ProjectOrder represents ways in which lists of projects can be ordered upon return. type ProjectOrder struct { // The field in which to order projects by. (Required.) @@ -276,6 +618,14 @@ type ProjectOrder struct { Direction OrderDirection `json:"direction"` } +// PullRequestOrder represents ways in which lists of issues can be ordered upon return. +type PullRequestOrder struct { + // The field in which to order pull requests by. (Required.) + Field PullRequestOrderField `json:"field"` + // The direction in which to order pull requests by the specified field. (Required.) + Direction OrderDirection `json:"direction"` +} + // ReactionOrder represents ways in which lists of reactions can be ordered upon return. type ReactionOrder struct { // The field in which to order reactions by. (Required.) @@ -300,6 +650,28 @@ type ReleaseOrder struct { Direction OrderDirection `json:"direction"` } +// RemoveAssigneesFromAssignableInput is an autogenerated input type of RemoveAssigneesFromAssignable. +type RemoveAssigneesFromAssignableInput struct { + // The id of the assignable object to remove assignees from. (Required.) + AssignableID ID `json:"assignableId"` + // The id of users to remove as assignees. (Required.) + AssigneeIDs []ID `json:"assigneeIds"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// RemoveLabelsFromLabelableInput is an autogenerated input type of RemoveLabelsFromLabelable. +type RemoveLabelsFromLabelableInput struct { + // The id of the Labelable to remove labels from. (Required.) + LabelableID ID `json:"labelableId"` + // The ids of labels to remove. (Required.) + LabelIDs []ID `json:"labelIds"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // RemoveOutsideCollaboratorInput is an autogenerated input type of RemoveOutsideCollaborator. type RemoveOutsideCollaboratorInput struct { // The ID of the outside collaborator to remove. (Required.) @@ -331,6 +703,24 @@ type RemoveStarInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// ReopenIssueInput is an autogenerated input type of ReopenIssue. +type ReopenIssueInput struct { + // ID of the issue to be opened. (Required.) + IssueID ID `json:"issueId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// ReopenPullRequestInput is an autogenerated input type of ReopenPullRequest. +type ReopenPullRequestInput struct { + // ID of the pull request to be reopened. (Required.) + PullRequestID ID `json:"pullRequestId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // RepositoryOrder represents ordering options for repository connections. type RepositoryOrder struct { // The field to order repositories by. (Required.) @@ -354,6 +744,39 @@ type RequestReviewsInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// ResolveReviewThreadInput is an autogenerated input type of ResolveReviewThread. +type ResolveReviewThreadInput struct { + // The ID of the thread to resolve. (Required.) + ThreadID ID `json:"threadId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// SecurityAdvisoryIdentifierFilter represents an advisory identifier to filter results on. +type SecurityAdvisoryIdentifierFilter struct { + // The identifier type. (Required.) + Type SecurityAdvisoryIdentifierType `json:"type"` + // The identifier string. Supports exact or partial matching. (Required.) + Value String `json:"value"` +} + +// SecurityAdvisoryOrder represents ordering options for security advisory connections. +type SecurityAdvisoryOrder struct { + // The field to order security advisories by. (Required.) + Field SecurityAdvisoryOrderField `json:"field"` + // The ordering direction. (Required.) + Direction OrderDirection `json:"direction"` +} + +// SecurityVulnerabilityOrder represents ordering options for security vulnerability connections. +type SecurityVulnerabilityOrder struct { + // The field to order security vulnerabilities by. (Required.) + Field SecurityVulnerabilityOrderField `json:"field"` + // The ordering direction. (Required.) + Direction OrderDirection `json:"direction"` +} + // StarOrder represents ways in which star connections can be ordered. type StarOrder struct { // The field in which to order nodes by. (Required.) @@ -399,13 +822,133 @@ type TeamRepositoryOrder struct { Direction OrderDirection `json:"direction"` } +// UnlockLockableInput is an autogenerated input type of UnlockLockable. +type UnlockLockableInput struct { + // ID of the issue or pull request to be unlocked. (Required.) + LockableID ID `json:"lockableId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// UnmarkIssueAsDuplicateInput is an autogenerated input type of UnmarkIssueAsDuplicate. +type UnmarkIssueAsDuplicateInput struct { + // ID of the issue or pull request currently marked as a duplicate. (Required.) + DuplicateID ID `json:"duplicateId"` + // ID of the issue or pull request currently considered canonical/authoritative/original. (Required.) + CanonicalID ID `json:"canonicalId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// UnminimizeCommentInput is an autogenerated input type of UnminimizeComment. +type UnminimizeCommentInput struct { + // The Node ID of the subject to modify. (Required.) + SubjectID ID `json:"subjectId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// UnpinIssueInput is an autogenerated input type of UnpinIssue. +type UnpinIssueInput struct { + // The ID of the issue to be unpinned. (Required.) + IssueID ID `json:"issueId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// UnresolveReviewThreadInput is an autogenerated input type of UnresolveReviewThread. +type UnresolveReviewThreadInput struct { + // The ID of the thread to unresolve. (Required.) + ThreadID ID `json:"threadId"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// UpdateBranchProtectionRuleInput is an autogenerated input type of UpdateBranchProtectionRule. +type UpdateBranchProtectionRuleInput struct { + // The global relay id of the branch protection rule to be updated. (Required.) + BranchProtectionRuleID ID `json:"branchProtectionRuleId"` + + // The glob-like pattern used to determine matching branches. (Optional.) + Pattern *String `json:"pattern,omitempty"` + // Are approving reviews required to update matching branches. (Optional.) + RequiresApprovingReviews *Boolean `json:"requiresApprovingReviews,omitempty"` + // Number of approving reviews required to update matching branches. (Optional.) + RequiredApprovingReviewCount *Int `json:"requiredApprovingReviewCount,omitempty"` + // Are commits required to be signed. (Optional.) + RequiresCommitSignatures *Boolean `json:"requiresCommitSignatures,omitempty"` + // Can admins overwrite branch protection. (Optional.) + IsAdminEnforced *Boolean `json:"isAdminEnforced,omitempty"` + // Are status checks required to update matching branches. (Optional.) + RequiresStatusChecks *Boolean `json:"requiresStatusChecks,omitempty"` + // Are branches required to be up to date before merging. (Optional.) + RequiresStrictStatusChecks *Boolean `json:"requiresStrictStatusChecks,omitempty"` + // Are reviews from code owners required to update matching branches. (Optional.) + RequiresCodeOwnerReviews *Boolean `json:"requiresCodeOwnerReviews,omitempty"` + // Will new commits pushed to matching branches dismiss pull request review approvals. (Optional.) + DismissesStaleReviews *Boolean `json:"dismissesStaleReviews,omitempty"` + // Is dismissal of pull request reviews restricted. (Optional.) + RestrictsReviewDismissals *Boolean `json:"restrictsReviewDismissals,omitempty"` + // A list of User or Team IDs allowed to dismiss reviews on pull requests targeting matching branches. (Optional.) + ReviewDismissalActorIDs *[]ID `json:"reviewDismissalActorIds,omitempty"` + // Is pushing to matching branches restricted. (Optional.) + RestrictsPushes *Boolean `json:"restrictsPushes,omitempty"` + // A list of User or Team IDs allowed to push to matching branches. (Optional.) + PushActorIDs *[]ID `json:"pushActorIds,omitempty"` + // List of required status check contexts that must pass for commits to be accepted to matching branches. (Optional.) + RequiredStatusCheckContexts *[]String `json:"requiredStatusCheckContexts,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// UpdateIssueCommentInput is an autogenerated input type of UpdateIssueComment. +type UpdateIssueCommentInput struct { + // The ID of the IssueComment to modify. (Required.) + ID ID `json:"id"` + // The updated text of the comment. (Required.) + Body String `json:"body"` + + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + +// UpdateIssueInput is an autogenerated input type of UpdateIssue. +type UpdateIssueInput struct { + // The ID of the Issue to modify. (Required.) + ID ID `json:"id"` + + // The title for the issue. (Optional.) + Title *String `json:"title,omitempty"` + // The body for the issue description. (Optional.) + Body *String `json:"body,omitempty"` + // An array of Node IDs of users for this issue. (Optional.) + AssigneeIDs *[]ID `json:"assigneeIds,omitempty"` + // The Node ID of the milestone for this issue. (Optional.) + MilestoneID *ID `json:"milestoneId,omitempty"` + // An array of Node IDs of labels for this issue. (Optional.) + LabelIDs *[]ID `json:"labelIds,omitempty"` + // The desired issue state. (Optional.) + State *IssueState `json:"state,omitempty"` + // An array of Node IDs for projects associated with this issue. (Optional.) + ProjectIDs *[]ID `json:"projectIds,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // UpdateProjectCardInput is an autogenerated input type of UpdateProjectCard. type UpdateProjectCardInput struct { // The ProjectCard ID to update. (Required.) ProjectCardID ID `json:"projectCardId"` - // The note of ProjectCard. (Required.) - Note String `json:"note"` + // Whether or not the ProjectCard should be archived. (Optional.) + IsArchived *Boolean `json:"isArchived,omitempty"` + // The note of ProjectCard. (Optional.) + Note *String `json:"note,omitempty"` // A unique identifier for the client performing the mutation. (Optional.) ClientMutationID *String `json:"clientMutationId,omitempty"` } @@ -438,6 +981,23 @@ type UpdateProjectInput struct { ClientMutationID *String `json:"clientMutationId,omitempty"` } +// UpdatePullRequestInput is an autogenerated input type of UpdatePullRequest. +type UpdatePullRequestInput struct { + // The Node ID of the pull request. (Required.) + PullRequestID ID `json:"pullRequestId"` + + // The name of the branch you want your changes pulled into. This should be an existing branch on the current repository. (Optional.) + BaseRefName *String `json:"baseRefName,omitempty"` + // The title of the pull request. (Optional.) + Title *String `json:"title,omitempty"` + // The contents of the pull request. (Optional.) + Body *String `json:"body,omitempty"` + // Indicates whether maintainers can modify the pull request. (Optional.) + MaintainerCanModify *Boolean `json:"maintainerCanModify,omitempty"` + // A unique identifier for the client performing the mutation. (Optional.) + ClientMutationID *String `json:"clientMutationId,omitempty"` +} + // UpdatePullRequestReviewCommentInput is an autogenerated input type of UpdatePullRequestReviewComment. type UpdatePullRequestReviewCommentInput struct { // The Node ID of the comment to modify. (Required.) @@ -481,3 +1041,11 @@ type UpdateTopicsInput struct { // A unique identifier for the client performing the mutation. (Optional.) ClientMutationID *String `json:"clientMutationId,omitempty"` } + +// UserStatusOrder represents ordering options for user status connections. +type UserStatusOrder struct { + // The field to order user statuses by. (Required.) + Field UserStatusOrderField `json:"field"` + // The ordering direction. (Required.) + Direction OrderDirection `json:"direction"` +} From 2f620e65b8edeb761a161cea0dda11eb1cdf8fec Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Fri, 14 Jun 2019 20:28:53 +0200 Subject: [PATCH 04/29] [bridge/github] exporter: add graphql functionalities [bridge/github] graphql mutation objects [bridge/github] github create and get labels [bridge/github] repository node_id query [bridge/github] exporter: ignore old bugs and old operations [bridge/github] add update labels/status functionalities [bridge/github] exporter: cache labels while exporting --- bridge/github/export.go | 442 +++++++++++++++++++++++++++++++++- bridge/github/export_query.go | 40 +++ bridge/github/export_test.go | 2 +- 3 files changed, 477 insertions(+), 7 deletions(-) create mode 100644 bridge/github/export_query.go diff --git a/bridge/github/export.go b/bridge/github/export.go index 3c07d682c0e9d3c8dff8ef3ba3acba5e9312373d..8fcbd9e85f352ca9754bf66b93515f16588f9716 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -1,19 +1,38 @@ package github import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "net/http" "time" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" + "github.com/shurcooL/githubv4" +) + +const ( + keyGithubIdExport = "github-id" + keyGithubUrlExport = "github-url" ) // githubImporter implement the Importer interface type githubExporter struct { - conf core.Configuration + gc *githubv4.Client + conf core.Configuration + cachedLabels map[string]githubv4.ID } +// Init . func (ge *githubExporter) Init(conf core.Configuration) error { + ge.gc = buildClient(conf["token"]) ge.conf = conf + ge.cachedLabels = make(map[string]githubv4.ID) return nil } @@ -26,7 +45,7 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) erro allBugsIds := repo.AllBugsIds() - // + // collect bugs bugs := make([]*cache.BugCache, 0) for _, id := range allBugsIds { b, err := repo.ResolveBug(id) @@ -34,16 +53,427 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) erro return err } - // check if user participated in the issue - participants := b.Snapshot().Participants - for _, p := range participants { + snapshot := b.Snapshot() + + // ignore issues edited before since date + if snapshot.LastEditTime().Before(since) { + continue + } + + // if identity participated in a bug + for _, p := range snapshot.Participants { if p.Id() == identity.Id() { bugs = append(bugs, b) } } } - //TODO: Export bugs/events/editions + // get repository node id + repositoryID, err := getRepositoryNodeID( + ge.conf[keyOwner], + ge.conf[keyProject], + ge.conf[keyToken], + ) + if err != nil { + return err + } + + for _, b := range bugs { + snapshot := b.Snapshot() + bugGithubID := "" + + for _, op := range snapshot.Operations { + // treat only operations after since date + if op.Time().Before(since) { + continue + } + + // ignore SetMetadata operations + if _, ok := op.(*bug.SetMetadataOperation); ok { + continue + } + + // ignore imported issues and operations from github + if _, ok := op.GetMetadata(keyGithubId); ok { + continue + } + + // get operation hash + hash, err := op.Hash() + if err != nil { + return fmt.Errorf("reading operation hash: %v", err) + } + + // ignore already exported issues and operations + if _, err := b.ResolveOperationWithMetadata("github-exported-op", hash.String()); err != nil { + continue + } + + switch op.(type) { + case *bug.CreateOperation: + opr := op.(*bug.CreateOperation) + //TODO export files + bugGithubID, err = ge.createGithubIssue(repositoryID, opr.Title, opr.Message) + if err != nil { + return fmt.Errorf("exporting bug %v: %v", b.HumanId(), err) + } + + case *bug.AddCommentOperation: + opr := op.(*bug.AddCommentOperation) + bugGithubID, err = ge.addCommentGithubIssue(bugGithubID, opr.Message) + if err != nil { + return fmt.Errorf("adding comment %v: %v", "", err) + } + + case *bug.EditCommentOperation: + opr := op.(*bug.EditCommentOperation) + if err := ge.editCommentGithubIssue(bugGithubID, opr.Message); err != nil { + return fmt.Errorf("editing comment %v: %v", "", err) + } + + case *bug.SetStatusOperation: + opr := op.(*bug.SetStatusOperation) + if err := ge.updateGithubIssueStatus(bugGithubID, opr.Status); err != nil { + return fmt.Errorf("updating status %v: %v", bugGithubID, err) + } + + case *bug.SetTitleOperation: + opr := op.(*bug.SetTitleOperation) + if err := ge.updateGithubIssueTitle(bugGithubID, opr.Title); err != nil { + return fmt.Errorf("editing comment %v: %v", bugGithubID, err) + } + + case *bug.LabelChangeOperation: + opr := op.(*bug.LabelChangeOperation) + if err := ge.updateGithubIssueLabels(bugGithubID, opr.Added, opr.Removed); err != nil { + return fmt.Errorf("updating labels %v: %v", bugGithubID, err) + } + + default: + // ignore other type of operations + } + + } + + if err := b.CommitAsNeeded(); err != nil { + return fmt.Errorf("bug commit: %v", err) + } + + fmt.Printf("debug: %v", bugGithubID) + } + + return nil +} + +// getRepositoryNodeID request github api v3 to get repository node id +func getRepositoryNodeID(owner, project, token string) (string, error) { + url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project) + + client := &http.Client{ + Timeout: defaultTimeout, + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + + // need the token for private repositories + req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) + + resp, err := client.Do(req) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("error retrieving repository node id %v", resp.StatusCode) + } + + aux := struct { + NodeID string `json:"node_id"` + }{} + + data, _ := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + err = json.Unmarshal(data, &aux) + if err != nil { + return "", err + } + + return aux.NodeID, nil +} + +func (ge *githubExporter) markOperationAsExported(b *cache.BugCache, opHash string) error { + return nil +} + +// get label from github +func (ge *githubExporter) getGithubLabelID(label string) (string, error) { + url := fmt.Sprintf("%s/repos/%s/%s/labels/%s", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject], label) + + client := &http.Client{ + Timeout: defaultTimeout, + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + + // need the token for private repositories + req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[keyToken])) + + resp, err := client.Do(req) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusFound { + return "", fmt.Errorf("error getting label: status code: %v", resp.StatusCode) + } + + aux := struct { + ID string `json:"id"` + NodeID string `json:"node_id"` + Color string `json:"color"` + }{} + + data, _ := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + err = json.Unmarshal(data, &aux) + if err != nil { + return "", err + } + + return aux.NodeID, nil +} + +// create github label using api v3 +func (ge *githubExporter) createGithubLabel(label, labelColor string) (string, error) { + url := fmt.Sprintf("%s/repos/%s/%s/labels", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject]) + + client := &http.Client{ + Timeout: defaultTimeout, + } + + req, err := http.NewRequest("POST", url, nil) + if err != nil { + return "", err + } + + // need the token for private repositories + req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[keyToken])) + + resp, err := client.Do(req) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusCreated { + return "", fmt.Errorf("error creating label: response status %v", resp.StatusCode) + } + + aux := struct { + ID string `json:"id"` + NodeID string `json:"node_id"` + Color string `json:"color"` + }{} + + data, _ := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + + err = json.Unmarshal(data, &aux) + if err != nil { + return "", err + } + + return aux.NodeID, nil +} + +// randomHexColor return a random hex color code +func randomHexColor() string { + bytes := make([]byte, 6) + if _, err := rand.Read(bytes); err != nil { + return "fffff" + } + + return hex.EncodeToString(bytes) +} + +func (ge *githubExporter) getOrCreateGithubLabelID(repositoryID, label string) (string, error) { + // try to get label id + labelID, err := ge.getGithubLabelID(label) + if err == nil { + return labelID, nil + } + + // random color + color := randomHexColor() + + // create label and return id + labelID, err = ge.createGithubLabel(label, color) + if err != nil { + return "", err + } + + return labelID, nil +} + +func (ge *githubExporter) getLabelsIDs(repositoryID string, labels []bug.Label) ([]githubv4.ID, error) { + ids := make([]githubv4.ID, 0, len(labels)) + var err error + + // check labels ids + for _, l := range labels { + label := string(l) + + id, ok := ge.cachedLabels[label] + if !ok { + // try to query label id + id, err = ge.getOrCreateGithubLabelID(repositoryID, label) + if err != nil { + return nil, fmt.Errorf("get or create github label: %v", err) + } + + // cache label id + ge.cachedLabels[label] = id + } + + ids = append(ids, githubv4.ID(id)) + } + + return ids, nil +} + +// create a github issue and return it ID +func (ge *githubExporter) createGithubIssue(repositoryID, title, body string) (string, error) { + m := &createIssueMutation{} + input := &githubv4.CreateIssueInput{ + RepositoryID: repositoryID, + Title: githubv4.String(title), + Body: (*githubv4.String)(&body), + } + + if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + return "", err + } + + return m.CreateIssue.Issue.ID, nil +} + +// add a comment to an issue and return it ID +func (ge *githubExporter) addCommentGithubIssue(subjectID string, body string) (string, error) { + m := &addCommentToIssueMutation{} + input := &githubv4.AddCommentInput{ + SubjectID: subjectID, + Body: githubv4.String(body), + } + + if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + return "", err + } + + return m.AddComment.CommentEdge.Node.ID, nil +} + +func (ge *githubExporter) editCommentGithubIssue(commentID, body string) error { + m := &updateIssueCommentMutation{} + input := &githubv4.UpdateIssueCommentInput{ + ID: commentID, + Body: githubv4.String(body), + } + + if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + return err + } + + return nil +} + +func (ge *githubExporter) updateGithubIssueStatus(id string, status bug.Status) error { + m := &updateIssueMutation{} + + // set state + state := githubv4.IssueStateClosed + if status == bug.OpenStatus { + state = githubv4.IssueStateOpen + } + + input := &githubv4.UpdateIssueInput{ + ID: id, + State: &state, + } + + if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + return err + } + + return nil +} + +func (ge *githubExporter) updateGithubIssueBody(id string, body string) error { + m := &updateIssueMutation{} + input := &githubv4.UpdateIssueInput{ + ID: id, + Body: (*githubv4.String)(&body), + } + + if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + return err + } + + return nil +} + +func (ge *githubExporter) updateGithubIssueTitle(id, title string) error { + m := &updateIssueMutation{} + input := &githubv4.UpdateIssueInput{ + ID: id, + Title: (*githubv4.String)(&title), + } + + if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + return err + } + + return nil +} + +// update github issue labels +func (ge *githubExporter) updateGithubIssueLabels(labelableID string, added, removed []bug.Label) error { + addedIDs, err := ge.getLabelsIDs(labelableID, added) + if err != nil { + return fmt.Errorf("getting added labels ids: %v", err) + } + + m := &updateIssueMutation{} + inputAdd := &githubv4.AddLabelsToLabelableInput{ + LabelableID: labelableID, + LabelIDs: addedIDs, + } + + // add labels + if err := ge.gc.Mutate(context.TODO(), m, inputAdd, nil); err != nil { + return err + } + + removedIDs, err := ge.getLabelsIDs(labelableID, added) + if err != nil { + return fmt.Errorf("getting added labels ids: %v", err) + } + + inputRemove := &githubv4.RemoveLabelsFromLabelableInput{ + LabelableID: labelableID, + LabelIDs: removedIDs, + } + + // remove label labels + if err := ge.gc.Mutate(context.TODO(), m, inputRemove, nil); err != nil { + return err + } return nil } diff --git a/bridge/github/export_query.go b/bridge/github/export_query.go new file mode 100644 index 0000000000000000000000000000000000000000..ff03df3430f99dac6099bf49ed71f54dc9eafa45 --- /dev/null +++ b/bridge/github/export_query.go @@ -0,0 +1,40 @@ +package github + +type createIssueMutation struct { + CreateIssue struct { + Issue struct { + ID string `graphql:"id"` + URL string `graphql:"url"` + } + } `graphql:"createIssue(input:$input)"` +} + +type updateIssueMutation struct { + UpdateIssue struct { + Issue struct { + ID string `graphql:"id"` + URL string `graphql:"url"` + } + } `graphql:"updateIssue(input:$input)"` +} + +type addCommentToIssueMutation struct { + AddComment struct { + CommentEdge struct { + Node struct { + ID string `graphql:"id"` + URL string `graphql:"url"` + } + } + } `graphql:"addComment(input:$input)"` +} + +type updateIssueCommentMutation struct { + IssueComment struct { + ID string `graphql:"id"` + URL string `graphql:"url"` + } `graphql:"addComment(input:$input)"` +} + +type removeLabelsMutation struct { +} diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 74e8a656df9a7b9ccfd84cf4c93ceb65ae8229f6..710b798c4c41cb6dc7ecf1518ae4d95f7531e04e 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -3,5 +3,5 @@ package github import "testing" func TestExporter(t *testing.T) { - + //TODO test strategy } From 6451dd2928d7299dbcc3c34b3e6b8a8050f4c796 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 15 Jun 2019 02:50:09 +0200 Subject: [PATCH 05/29] [cache] BugCache: Add set metadata methods [bridge/github] fix add/remove mutation requests [bridge/github] iterator: fix typo --- bridge/github/export.go | 13 +++++-------- bridge/github/export_query.go | 9 +++++++-- bridge/github/iterator.go | 2 +- cache/bug_cache.go | 18 ++++++++++++++++++ 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index 8fcbd9e85f352ca9754bf66b93515f16588f9716..37207df70279e695ad8561ceffac2afb35a1f72f 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -10,15 +10,11 @@ import ( "net/http" "time" + "github.com/shurcooL/githubv4" + "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" - "github.com/shurcooL/githubv4" -) - -const ( - keyGithubIdExport = "github-id" - keyGithubUrlExport = "github-url" ) // githubImporter implement the Importer interface @@ -449,7 +445,7 @@ func (ge *githubExporter) updateGithubIssueLabels(labelableID string, added, rem return fmt.Errorf("getting added labels ids: %v", err) } - m := &updateIssueMutation{} + m := &addLabelsToLabelableMutation{} inputAdd := &githubv4.AddLabelsToLabelableInput{ LabelableID: labelableID, LabelIDs: addedIDs, @@ -465,13 +461,14 @@ func (ge *githubExporter) updateGithubIssueLabels(labelableID string, added, rem return fmt.Errorf("getting added labels ids: %v", err) } + m2 := &removeLabelsFromLabelableMutation{} inputRemove := &githubv4.RemoveLabelsFromLabelableInput{ LabelableID: labelableID, LabelIDs: removedIDs, } // remove label labels - if err := ge.gc.Mutate(context.TODO(), m, inputRemove, nil); err != nil { + if err := ge.gc.Mutate(context.TODO(), m2, inputRemove, nil); err != nil { return err } diff --git a/bridge/github/export_query.go b/bridge/github/export_query.go index ff03df3430f99dac6099bf49ed71f54dc9eafa45..a527399e61ee8eb9f685e6f080dfc27bee7d1d87 100644 --- a/bridge/github/export_query.go +++ b/bridge/github/export_query.go @@ -33,8 +33,13 @@ type updateIssueCommentMutation struct { IssueComment struct { ID string `graphql:"id"` URL string `graphql:"url"` - } `graphql:"addComment(input:$input)"` + } `graphql:"updateIssueComment(input:$input)"` +} + +type removeLabelsFromLabelableMutation struct { + AddLabels struct{} `graphql:"removeLabelsFromLabelable(input:$input)"` } -type removeLabelsMutation struct { +type addLabelsToLabelableMutation struct { + RemoveLabels struct{} `graphql:"addLabelsToLabelable(input:$input)"` } diff --git a/bridge/github/iterator.go b/bridge/github/iterator.go index fcf72b8f3fb258121f9721003c9416dcd1daabaf..3620f50d8030601d087173246def02682122e0d8 100644 --- a/bridge/github/iterator.go +++ b/bridge/github/iterator.go @@ -59,7 +59,7 @@ type iterator struct { commentEdit commentEditIterator } -// NewIterator create and initalize a new iterator +// NewIterator create and initialize a new iterator func NewIterator(owner, project, token string, since time.Time) *iterator { i := &iterator{ gc: buildClient(token), diff --git a/cache/bug_cache.go b/cache/bug_cache.go index aad6dab81cff4997a1efbc81242d33eac85f2cff..36f2139edb1f0c8929ff00f12d243ab159e47d1c 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -254,6 +254,24 @@ func (c *BugCache) EditCommentRaw(author *IdentityCache, unixTime int64, target return op, c.notifyUpdated() } +func (c *BugCache) SetMetadata(target git.Hash, newMetadata map[string]string) (*bug.SetMetadataOperation, error) { + author, err := c.repoCache.GetUserIdentity() + if err != nil { + return nil, err + } + + return c.SetMetadataRaw(author, time.Now().Unix(), target, nil) +} + +func (c *BugCache) SetMetadataRaw(author *IdentityCache, unixTime int64, target git.Hash, newMetadata map[string]string) (*bug.SetMetadataOperation, error) { + op, err := bug.SetMetadata(c.bug, author.Identity, unixTime, target, newMetadata) + if err != nil { + return nil, err + } + + return op, c.notifyUpdated() +} + func (c *BugCache) Commit() error { err := c.bug.Commit(c.repoCache.repo) if err != nil { From cbac48277000b6d6c85418f099c1daf0e4d64956 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Thu, 20 Jun 2019 18:08:29 +0200 Subject: [PATCH 06/29] [bridge/github] update github bug exporter - cache repository ID - cache all comment IDs --- bridge/github/export.go | 328 +++++++++++++++++++++++++++++----------- 1 file changed, 240 insertions(+), 88 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index 37207df70279e695ad8561ceffac2afb35a1f72f..a703e1e243e4362b5acd301c74d045d9dda3540a 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -15,34 +15,54 @@ import ( "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/identity" + "github.com/MichaelMure/git-bug/util/git" ) // githubImporter implement the Importer interface type githubExporter struct { - gc *githubv4.Client - conf core.Configuration - cachedLabels map[string]githubv4.ID + gc *githubv4.Client + conf core.Configuration + + // github repository ID + repositoryID string + + // cache identifiers used to speed up exporting operations + // cleared for each bug + cachedIDs map[string]string + + // cache labels used to speed up exporting labels events + cachedLabels map[string]string } // Init . func (ge *githubExporter) Init(conf core.Configuration) error { - ge.gc = buildClient(conf["token"]) ge.conf = conf - ge.cachedLabels = make(map[string]githubv4.ID) + ge.gc = buildClient(conf["token"]) + ge.cachedIDs = make(map[string]string) + ge.cachedLabels = make(map[string]string) return nil } // ExportAll export all event made by the current user to Github func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) error { - identity, err := repo.GetUserIdentity() + user, err := repo.GetUserIdentity() if err != nil { return err } - allBugsIds := repo.AllBugsIds() + // get repository node id + ge.repositoryID, err = getRepositoryNodeID( + ge.conf[keyOwner], + ge.conf[keyProject], + ge.conf[keyToken], + ) - // collect bugs - bugs := make([]*cache.BugCache, 0) + if err != nil { + return err + } + + allBugsIds := repo.AllBugsIds() for _, id := range allBugsIds { b, err := repo.ResolveBug(id) if err != nil { @@ -51,111 +71,229 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) erro snapshot := b.Snapshot() - // ignore issues edited before since date - if snapshot.LastEditTime().Before(since) { + // ignore issues created before since date + if snapshot.CreatedAt.Before(since) { continue } // if identity participated in a bug for _, p := range snapshot.Participants { - if p.Id() == identity.Id() { - bugs = append(bugs, b) + if p.Id() == user.Id() { + // try to export the bug and it associated events + if err := ge.exportBug(b, user.Identity, since); err != nil { + return err + } } } } - // get repository node id - repositoryID, err := getRepositoryNodeID( - ge.conf[keyOwner], - ge.conf[keyProject], - ge.conf[keyToken], - ) + return nil +} + +// exportBug publish bugs and related events +func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, since time.Time) error { + snapshot := b.Snapshot() + + var bugGithubID string + var bugGithubURL string + var bugCreationHash string + + // Special case: + // if a user try to export a bug that is not already exported to Github (or imported + // from Github) and he is not the author of the bug. There is nothing we can do. + + // first operation is always createOp + createOp := snapshot.Operations[0].(*bug.CreateOperation) + bugAuthorID := createOp.OpBase.Author.Id() + + // get github bug ID + githubID, ok := createOp.GetMetadata(keyGithubId) + if ok { + githubURL, ok := createOp.GetMetadata(keyGithubId) + if !ok { + // if we find github ID, github URL must be found too + panic("expected to find github issue URL") + } + + // will be used to mark operation related to a bug as exported + bugGithubID = githubID + bugGithubURL = githubURL + + } else if !ok && bugAuthorID == user.Id() { + // create bug + id, url, err := ge.createGithubIssue(ge.repositoryID, createOp.Title, createOp.Message) + if err != nil { + return fmt.Errorf("creating exporting github issue %v", err) + } + + hash, err := createOp.Hash() + if err != nil { + return fmt.Errorf("comment hash: %v", err) + } + + // mark bug creation operation as exported + if err := markOperationAsExported(b, hash, id, url); err != nil { + return fmt.Errorf("marking operation as exported: %v", err) + } + + // cache bug github ID and URL + bugGithubID = id + bugGithubURL = url + } else { + // if bug is still not exported and user cannot author bug stop the execution + + //TODO: maybe print a warning ? + // this is not an error + return nil + } + + // get createOp hash + hash, err := createOp.Hash() if err != nil { return err } - for _, b := range bugs { - snapshot := b.Snapshot() - bugGithubID := "" + bugCreationHash = hash.String() - for _, op := range snapshot.Operations { - // treat only operations after since date - if op.Time().Before(since) { - continue - } + // cache operation github id + ge.cachedIDs[bugCreationHash] = bugGithubID - // ignore SetMetadata operations - if _, ok := op.(*bug.SetMetadataOperation); ok { - continue - } + for _, op := range snapshot.Operations[1:] { + // ignore SetMetadata operations + if _, ok := op.(*bug.SetMetadataOperation); ok { + continue + } - // ignore imported issues and operations from github - if _, ok := op.GetMetadata(keyGithubId); ok { - continue + // get operation hash + hash, err := op.Hash() + if err != nil { + return fmt.Errorf("reading operation hash: %v", err) + } + + // ignore imported (or exported) operations from github + // cache the ID of already exported or imported issues and events from Github + if id, ok := op.GetMetadata(keyGithubId); ok { + ge.cachedIDs[hash.String()] = id + continue + } + + switch op.(type) { + case *bug.AddCommentOperation: + opr := op.(*bug.AddCommentOperation) + + // send operation to github + id, url, err := ge.addCommentGithubIssue(bugGithubID, opr.Message) + if err != nil { + return fmt.Errorf("adding comment: %v", err) } - // get operation hash - hash, err := op.Hash() + hash, err := opr.Hash() if err != nil { - return fmt.Errorf("reading operation hash: %v", err) + return fmt.Errorf("comment hash: %v", err) } - // ignore already exported issues and operations - if _, err := b.ResolveOperationWithMetadata("github-exported-op", hash.String()); err != nil { - continue + // mark operation as exported + if err := markOperationAsExported(b, hash, id, url); err != nil { + return fmt.Errorf("marking operation as exported: %v", err) } - switch op.(type) { - case *bug.CreateOperation: - opr := op.(*bug.CreateOperation) - //TODO export files - bugGithubID, err = ge.createGithubIssue(repositoryID, opr.Title, opr.Message) - if err != nil { - return fmt.Errorf("exporting bug %v: %v", b.HumanId(), err) - } + case *bug.EditCommentOperation: - case *bug.AddCommentOperation: - opr := op.(*bug.AddCommentOperation) - bugGithubID, err = ge.addCommentGithubIssue(bugGithubID, opr.Message) - if err != nil { - return fmt.Errorf("adding comment %v: %v", "", err) - } + id := bugGithubID + url := bugGithubURL + opr := op.(*bug.EditCommentOperation) + targetHash := opr.Target.String() - case *bug.EditCommentOperation: - opr := op.(*bug.EditCommentOperation) - if err := ge.editCommentGithubIssue(bugGithubID, opr.Message); err != nil { - return fmt.Errorf("editing comment %v: %v", "", err) + // Since github doesn't consider the issue body as a comment + if targetHash == bugCreationHash { + // case bug creation operation: we need to edit the Github issue + if err := ge.updateGithubIssueBody(bugGithubID, opr.Message); err != nil { + return fmt.Errorf("editing issue: %v", err) } - case *bug.SetStatusOperation: - opr := op.(*bug.SetStatusOperation) - if err := ge.updateGithubIssueStatus(bugGithubID, opr.Status); err != nil { - return fmt.Errorf("updating status %v: %v", bugGithubID, err) + } else { + // case comment edition operation: we need to edit the Github comment + commentID, ok := ge.cachedIDs[targetHash] + if !ok { + panic("unexpected error: comment id not found") } - case *bug.SetTitleOperation: - opr := op.(*bug.SetTitleOperation) - if err := ge.updateGithubIssueTitle(bugGithubID, opr.Title); err != nil { - return fmt.Errorf("editing comment %v: %v", bugGithubID, err) + eid, eurl, err := ge.editCommentGithubIssue(commentID, opr.Message) + if err != nil { + return fmt.Errorf("editing comment: %v", err) } - case *bug.LabelChangeOperation: - opr := op.(*bug.LabelChangeOperation) - if err := ge.updateGithubIssueLabels(bugGithubID, opr.Added, opr.Removed); err != nil { - return fmt.Errorf("updating labels %v: %v", bugGithubID, err) - } + // use comment id/url instead of issue id/url + id = eid + url = eurl + } + + hash, err := opr.Hash() + if err != nil { + return fmt.Errorf("comment hash: %v", err) + } - default: - // ignore other type of operations + // mark operation as exported + if err := markOperationAsExported(b, hash, id, url); err != nil { + return fmt.Errorf("marking operation as exported: %v", err) } - } + case *bug.SetStatusOperation: + opr := op.(*bug.SetStatusOperation) + if err := ge.updateGithubIssueStatus(bugGithubID, opr.Status); err != nil { + return fmt.Errorf("updating status %v: %v", bugGithubID, err) + } + + hash, err := opr.Hash() + if err != nil { + return fmt.Errorf("comment hash: %v", err) + } + + // mark operation as exported + if err := markOperationAsExported(b, hash, bugGithubID, bugGithubURL); err != nil { + return fmt.Errorf("marking operation as exported: %v", err) + } - if err := b.CommitAsNeeded(); err != nil { - return fmt.Errorf("bug commit: %v", err) + case *bug.SetTitleOperation: + opr := op.(*bug.SetTitleOperation) + if err := ge.updateGithubIssueTitle(bugGithubID, opr.Title); err != nil { + return fmt.Errorf("editing comment %v: %v", bugGithubID, err) + } + + hash, err := opr.Hash() + if err != nil { + return fmt.Errorf("comment hash: %v", err) + } + + // mark operation as exported + if err := markOperationAsExported(b, hash, bugGithubID, bugGithubURL); err != nil { + return fmt.Errorf("marking operation as exported: %v", err) + } + + case *bug.LabelChangeOperation: + opr := op.(*bug.LabelChangeOperation) + if err := ge.updateGithubIssueLabels(bugGithubID, opr.Added, opr.Removed); err != nil { + return fmt.Errorf("updating labels %v: %v", bugGithubID, err) + } + + hash, err := opr.Hash() + if err != nil { + return fmt.Errorf("comment hash: %v", err) + } + + // mark operation as exported + if err := markOperationAsExported(b, hash, bugGithubID, bugGithubURL); err != nil { + return fmt.Errorf("marking operation as exported: %v", err) + } + + default: + panic("unhandled operation type case") } - fmt.Printf("debug: %v", bugGithubID) + } + + if err := b.CommitAsNeeded(); err != nil { + return fmt.Errorf("bug commit: %v", err) } return nil @@ -201,8 +339,16 @@ func getRepositoryNodeID(owner, project, token string) (string, error) { return aux.NodeID, nil } -func (ge *githubExporter) markOperationAsExported(b *cache.BugCache, opHash string) error { - return nil +func markOperationAsExported(b *cache.BugCache, target git.Hash, githubID, githubURL string) error { + _, err := b.SetMetadata( + target, + map[string]string{ + keyGithubId: githubID, + keyGithubUrl: githubURL, + }, + ) + + return err } // get label from github @@ -296,6 +442,8 @@ func randomHexColor() string { return "fffff" } + // fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) + return hex.EncodeToString(bytes) } @@ -307,6 +455,7 @@ func (ge *githubExporter) getOrCreateGithubLabelID(repositoryID, label string) ( } // random color + //TODO: no random color := randomHexColor() // create label and return id @@ -345,7 +494,7 @@ func (ge *githubExporter) getLabelsIDs(repositoryID string, labels []bug.Label) } // create a github issue and return it ID -func (ge *githubExporter) createGithubIssue(repositoryID, title, body string) (string, error) { +func (ge *githubExporter) createGithubIssue(repositoryID, title, body string) (string, string, error) { m := &createIssueMutation{} input := &githubv4.CreateIssueInput{ RepositoryID: repositoryID, @@ -354,14 +503,15 @@ func (ge *githubExporter) createGithubIssue(repositoryID, title, body string) (s } if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { - return "", err + return "", "", err } - return m.CreateIssue.Issue.ID, nil + issue := m.CreateIssue.Issue + return issue.ID, issue.URL, nil } // add a comment to an issue and return it ID -func (ge *githubExporter) addCommentGithubIssue(subjectID string, body string) (string, error) { +func (ge *githubExporter) addCommentGithubIssue(subjectID string, body string) (string, string, error) { m := &addCommentToIssueMutation{} input := &githubv4.AddCommentInput{ SubjectID: subjectID, @@ -369,13 +519,14 @@ func (ge *githubExporter) addCommentGithubIssue(subjectID string, body string) ( } if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { - return "", err + return "", "", err } - return m.AddComment.CommentEdge.Node.ID, nil + node := m.AddComment.CommentEdge.Node + return node.ID, node.URL, nil } -func (ge *githubExporter) editCommentGithubIssue(commentID, body string) error { +func (ge *githubExporter) editCommentGithubIssue(commentID, body string) (string, string, error) { m := &updateIssueCommentMutation{} input := &githubv4.UpdateIssueCommentInput{ ID: commentID, @@ -383,10 +534,11 @@ func (ge *githubExporter) editCommentGithubIssue(commentID, body string) error { } if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { - return err + return "", "", err } - return nil + comment := m.IssueComment + return commentID, comment.URL, nil } func (ge *githubExporter) updateGithubIssueStatus(id string, status bug.Status) error { From e649a503963887c1fe5cdc3deaa0e2951261d76b Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Thu, 20 Jun 2019 23:56:49 +0200 Subject: [PATCH 07/29] [bug] Operations: add GetAuthor method for operation interface and BaseOp --- bug/operation.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bug/operation.go b/bug/operation.go index 8e77eed83c97d71584eaad8596343727045acd44..2cb27d18cba34a021ba53fa76d2c85da8b60a7d8 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -49,6 +49,8 @@ type Operation interface { GetMetadata(key string) (string, bool) // AllMetadata return all metadata for this operation AllMetadata() map[string]string + // GetAuthor return author identity + GetAuthor() identity.Interface } func hashRaw(data []byte) git.Hash { @@ -222,3 +224,7 @@ func (op *OpBase) AllMetadata() map[string]string { return result } + +func (op *OpBase) GetAuthor() identity.Interface { + return op.Author +} From 6d9ba4aba21f9b7bd8322594678604966eca777e Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Fri, 21 Jun 2019 00:09:20 +0200 Subject: [PATCH 08/29] rebase & fix push command --- commands/bridge_push.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/bridge_push.go b/commands/bridge_push.go index 19b5fc4f2d562be99caadf269f4bea70a5b3cbf9..8cae1684c91157c89bba6ef7b622f16a9a7e9154 100644 --- a/commands/bridge_push.go +++ b/commands/bridge_push.go @@ -24,7 +24,7 @@ func runBridgePush(cmd *cobra.Command, args []string) error { if len(args) == 0 { b, err = bridge.DefaultBridge(backend) } else { - b, err = bridge.NewBridgeFromFullName(backend, args[0]) + b, err = bridge.LoadBridge(backend, args[0]) } if err != nil { From a121991f556f5b2a362d769b9304956bcedb3805 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Fri, 21 Jun 2019 00:25:43 +0200 Subject: [PATCH 09/29] [bridge/github] exporter: support multiple tokens/clients --- bridge/github/export.go | 145 +++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 62 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index a703e1e243e4362b5acd301c74d045d9dda3540a..32df82408eb4ee8356e1a1d9468224860d98e35c 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -24,6 +24,12 @@ type githubExporter struct { gc *githubv4.Client conf core.Configuration + // a map containing + tokens map[string]string + + // clients + clients map[string]*githubv4.Client + // github repository ID repositoryID string @@ -37,13 +43,30 @@ type githubExporter struct { // Init . func (ge *githubExporter) Init(conf core.Configuration) error { + //TODO: initialize with multiple tokens ge.conf = conf ge.gc = buildClient(conf["token"]) + ge.tokens = make(map[string]string) + ge.clients = make(map[string]*githubv4.Client) ge.cachedIDs = make(map[string]string) ge.cachedLabels = make(map[string]string) return nil } +func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) { + client, ok := ge.clients[id] + if ok { + return client, nil + } + + token, ok := ge.tokens[id] + if !ok { + return nil, fmt.Errorf("unknown identity token") + } + + return buildClient(token), nil +} + // ExportAll export all event made by the current user to Github func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) error { user, err := repo.GetUserIdentity() @@ -104,7 +127,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, // first operation is always createOp createOp := snapshot.Operations[0].(*bug.CreateOperation) - bugAuthorID := createOp.OpBase.Author.Id() + author := createOp.GetAuthor() // get github bug ID githubID, ok := createOp.GetMetadata(keyGithubId) @@ -119,9 +142,19 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, bugGithubID = githubID bugGithubURL = githubURL - } else if !ok && bugAuthorID == user.Id() { + } else { + client, err := ge.getIdentityClient(author.Id()) + if err != nil { + // if bug is still not exported and user cannot author bug stop the execution + + // TODO: maybe print a warning ? + // this is not an error + // don't export bug + return nil + } + // create bug - id, url, err := ge.createGithubIssue(ge.repositoryID, createOp.Title, createOp.Message) + id, url, err := createGithubIssue(client, ge.repositoryID, createOp.Title, createOp.Message) if err != nil { return fmt.Errorf("creating exporting github issue %v", err) } @@ -139,12 +172,6 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, // cache bug github ID and URL bugGithubID = id bugGithubURL = url - } else { - // if bug is still not exported and user cannot author bug stop the execution - - //TODO: maybe print a warning ? - // this is not an error - return nil } // get createOp hash @@ -177,40 +204,43 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, continue } + opAuthor := op.GetAuthor() + client, err := ge.getIdentityClient(opAuthor.Id()) + if err != nil { + // don't export operation + continue + } + + var id, url string switch op.(type) { case *bug.AddCommentOperation: opr := op.(*bug.AddCommentOperation) // send operation to github - id, url, err := ge.addCommentGithubIssue(bugGithubID, opr.Message) + id, url, err = addCommentGithubIssue(client, bugGithubID, opr.Message) if err != nil { return fmt.Errorf("adding comment: %v", err) } - hash, err := opr.Hash() + hash, err = opr.Hash() if err != nil { return fmt.Errorf("comment hash: %v", err) } - // mark operation as exported - if err := markOperationAsExported(b, hash, id, url); err != nil { - return fmt.Errorf("marking operation as exported: %v", err) - } - case *bug.EditCommentOperation: - - id := bugGithubID - url := bugGithubURL opr := op.(*bug.EditCommentOperation) targetHash := opr.Target.String() // Since github doesn't consider the issue body as a comment if targetHash == bugCreationHash { // case bug creation operation: we need to edit the Github issue - if err := ge.updateGithubIssueBody(bugGithubID, opr.Message); err != nil { + if err := updateGithubIssueBody(client, bugGithubID, opr.Message); err != nil { return fmt.Errorf("editing issue: %v", err) } + id = bugGithubID + url = bugGithubURL + } else { // case comment edition operation: we need to edit the Github comment commentID, ok := ge.cachedIDs[targetHash] @@ -218,7 +248,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, panic("unexpected error: comment id not found") } - eid, eurl, err := ge.editCommentGithubIssue(commentID, opr.Message) + eid, eurl, err := editCommentGithubIssue(client, commentID, opr.Message) if err != nil { return fmt.Errorf("editing comment: %v", err) } @@ -228,68 +258,61 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, url = eurl } - hash, err := opr.Hash() + hash, err = opr.Hash() if err != nil { return fmt.Errorf("comment hash: %v", err) } - // mark operation as exported - if err := markOperationAsExported(b, hash, id, url); err != nil { - return fmt.Errorf("marking operation as exported: %v", err) - } - case *bug.SetStatusOperation: opr := op.(*bug.SetStatusOperation) - if err := ge.updateGithubIssueStatus(bugGithubID, opr.Status); err != nil { + if err := updateGithubIssueStatus(client, bugGithubID, opr.Status); err != nil { return fmt.Errorf("updating status %v: %v", bugGithubID, err) } - hash, err := opr.Hash() + hash, err = opr.Hash() if err != nil { return fmt.Errorf("comment hash: %v", err) } - // mark operation as exported - if err := markOperationAsExported(b, hash, bugGithubID, bugGithubURL); err != nil { - return fmt.Errorf("marking operation as exported: %v", err) - } + id = bugGithubID + url = bugGithubURL case *bug.SetTitleOperation: opr := op.(*bug.SetTitleOperation) - if err := ge.updateGithubIssueTitle(bugGithubID, opr.Title); err != nil { + if err := updateGithubIssueTitle(client, bugGithubID, opr.Title); err != nil { return fmt.Errorf("editing comment %v: %v", bugGithubID, err) } - hash, err := opr.Hash() + hash, err = opr.Hash() if err != nil { return fmt.Errorf("comment hash: %v", err) } - // mark operation as exported - if err := markOperationAsExported(b, hash, bugGithubID, bugGithubURL); err != nil { - return fmt.Errorf("marking operation as exported: %v", err) - } + id = bugGithubID + url = bugGithubURL case *bug.LabelChangeOperation: opr := op.(*bug.LabelChangeOperation) - if err := ge.updateGithubIssueLabels(bugGithubID, opr.Added, opr.Removed); err != nil { + if err := ge.updateGithubIssueLabels(client, bugGithubID, opr.Added, opr.Removed); err != nil { return fmt.Errorf("updating labels %v: %v", bugGithubID, err) } - hash, err := opr.Hash() + hash, err = opr.Hash() if err != nil { return fmt.Errorf("comment hash: %v", err) } - // mark operation as exported - if err := markOperationAsExported(b, hash, bugGithubID, bugGithubURL); err != nil { - return fmt.Errorf("marking operation as exported: %v", err) - } + id = bugGithubID + url = bugGithubURL default: panic("unhandled operation type case") } + // mark operation as exported + if err := markOperationAsExported(b, hash, id, url); err != nil { + return fmt.Errorf("marking operation as exported: %v", err) + } } if err := b.CommitAsNeeded(); err != nil { @@ -442,8 +465,6 @@ func randomHexColor() string { return "fffff" } - // fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) - return hex.EncodeToString(bytes) } @@ -494,7 +515,7 @@ func (ge *githubExporter) getLabelsIDs(repositoryID string, labels []bug.Label) } // create a github issue and return it ID -func (ge *githubExporter) createGithubIssue(repositoryID, title, body string) (string, string, error) { +func createGithubIssue(gc *githubv4.Client, repositoryID, title, body string) (string, string, error) { m := &createIssueMutation{} input := &githubv4.CreateIssueInput{ RepositoryID: repositoryID, @@ -502,7 +523,7 @@ func (ge *githubExporter) createGithubIssue(repositoryID, title, body string) (s Body: (*githubv4.String)(&body), } - if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { return "", "", err } @@ -511,14 +532,14 @@ func (ge *githubExporter) createGithubIssue(repositoryID, title, body string) (s } // add a comment to an issue and return it ID -func (ge *githubExporter) addCommentGithubIssue(subjectID string, body string) (string, string, error) { +func addCommentGithubIssue(gc *githubv4.Client, subjectID string, body string) (string, string, error) { m := &addCommentToIssueMutation{} input := &githubv4.AddCommentInput{ SubjectID: subjectID, Body: githubv4.String(body), } - if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { return "", "", err } @@ -526,14 +547,14 @@ func (ge *githubExporter) addCommentGithubIssue(subjectID string, body string) ( return node.ID, node.URL, nil } -func (ge *githubExporter) editCommentGithubIssue(commentID, body string) (string, string, error) { +func editCommentGithubIssue(gc *githubv4.Client, commentID, body string) (string, string, error) { m := &updateIssueCommentMutation{} input := &githubv4.UpdateIssueCommentInput{ ID: commentID, Body: githubv4.String(body), } - if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { return "", "", err } @@ -541,7 +562,7 @@ func (ge *githubExporter) editCommentGithubIssue(commentID, body string) (string return commentID, comment.URL, nil } -func (ge *githubExporter) updateGithubIssueStatus(id string, status bug.Status) error { +func updateGithubIssueStatus(gc *githubv4.Client, id string, status bug.Status) error { m := &updateIssueMutation{} // set state @@ -555,35 +576,35 @@ func (ge *githubExporter) updateGithubIssueStatus(id string, status bug.Status) State: &state, } - if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { return err } return nil } -func (ge *githubExporter) updateGithubIssueBody(id string, body string) error { +func updateGithubIssueBody(gc *githubv4.Client, id string, body string) error { m := &updateIssueMutation{} input := &githubv4.UpdateIssueInput{ ID: id, Body: (*githubv4.String)(&body), } - if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { return err } return nil } -func (ge *githubExporter) updateGithubIssueTitle(id, title string) error { +func updateGithubIssueTitle(gc *githubv4.Client, id, title string) error { m := &updateIssueMutation{} input := &githubv4.UpdateIssueInput{ ID: id, Title: (*githubv4.String)(&title), } - if err := ge.gc.Mutate(context.TODO(), m, input, nil); err != nil { + if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { return err } @@ -591,7 +612,7 @@ func (ge *githubExporter) updateGithubIssueTitle(id, title string) error { } // update github issue labels -func (ge *githubExporter) updateGithubIssueLabels(labelableID string, added, removed []bug.Label) error { +func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelableID string, added, removed []bug.Label) error { addedIDs, err := ge.getLabelsIDs(labelableID, added) if err != nil { return fmt.Errorf("getting added labels ids: %v", err) @@ -604,7 +625,7 @@ func (ge *githubExporter) updateGithubIssueLabels(labelableID string, added, rem } // add labels - if err := ge.gc.Mutate(context.TODO(), m, inputAdd, nil); err != nil { + if err := gc.Mutate(context.TODO(), m, inputAdd, nil); err != nil { return err } @@ -620,7 +641,7 @@ func (ge *githubExporter) updateGithubIssueLabels(labelableID string, added, rem } // remove label labels - if err := ge.gc.Mutate(context.TODO(), m2, inputRemove, nil); err != nil { + if err := gc.Mutate(context.TODO(), m2, inputRemove, nil); err != nil { return err } From 1d42814166f783766a1b81e926c93c21244289dc Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Fri, 21 Jun 2019 01:09:05 +0200 Subject: [PATCH 10/29] [bridge/github] cache user token fix import typo init tests verify issue --- bridge/github/export.go | 17 +++++---- bridge/github/export_test.go | 67 +++++++++++++++++++++++++++++++++++- bridge/github/import_test.go | 2 +- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index 32df82408eb4ee8356e1a1d9468224860d98e35c..e761136065b919f63e19de18748b9adbb2068b61 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -21,7 +21,6 @@ import ( // githubImporter implement the Importer interface type githubExporter struct { - gc *githubv4.Client conf core.Configuration // a map containing @@ -45,7 +44,6 @@ type githubExporter struct { func (ge *githubExporter) Init(conf core.Configuration) error { //TODO: initialize with multiple tokens ge.conf = conf - ge.gc = buildClient(conf["token"]) ge.tokens = make(map[string]string) ge.clients = make(map[string]*githubv4.Client) ge.cachedIDs = make(map[string]string) @@ -74,6 +72,8 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) erro return err } + ge.tokens[user.Id()] = ge.conf[keyToken] + // get repository node id ge.repositoryID, err = getRepositoryNodeID( ge.conf[keyOwner], @@ -86,6 +86,7 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) erro } allBugsIds := repo.AllBugsIds() +bugLoop: for _, id := range allBugsIds { b, err := repo.ResolveBug(id) if err != nil { @@ -101,10 +102,14 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) erro // if identity participated in a bug for _, p := range snapshot.Participants { - if p.Id() == user.Id() { - // try to export the bug and it associated events - if err := ge.exportBug(b, user.Identity, since); err != nil { - return err + for userId := range ge.tokens { + if p.Id() == userId { + // try to export the bug and it associated events + if err := ge.exportBug(b, user.Identity, since); err != nil { + return err + } + + continue bugLoop } } } diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 710b798c4c41cb6dc7ecf1518ae4d95f7531e04e..c3a835b2bbe84ce937ab3c93356e447b756d76fa 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -1,7 +1,72 @@ package github -import "testing" +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/cache" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/util/interrupt" + "github.com/stretchr/testify/require" +) func TestExporter(t *testing.T) { //TODO test strategy + tests := []struct { + name string + }{ + { + name: "bug creation", + }, + } + + repo := repository.CreateTestRepo(false) + defer repository.CleanupTestRepos(t, repo) + + backend, err := cache.NewRepoCache(repo) + require.NoError(t, err) + + defer backend.Close() + interrupt.RegisterCleaner(backend.Close) + + token := os.Getenv("GITHUB_TOKEN_PRIVATE") + if token == "" { + t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") + } + + exporter := &githubExporter{} + err = exporter.Init(core.Configuration{ + keyOwner: "MichaelMure", + keyProject: "git-bug-exporter-tests", + keyToken: token, + }) + require.NoError(t, err) + + start := time.Now() + + err = exporter.ExportAll(backend, time.Time{}) + require.NoError(t, err) + + fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds()) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + }) + } +} + +func genRepoName() {} + +func createRepository() {} + +func deleteRepository() {} + +// verifyIssue is full +// comments +func verifyIssue() { + } diff --git a/bridge/github/import_test.go b/bridge/github/import_test.go index 24356f344496a1ca9f246e4071a711851c2a9db2..5ba879939973f94c8f0d97f37488eeaca3fe1517 100644 --- a/bridge/github/import_test.go +++ b/bridge/github/import_test.go @@ -190,7 +190,7 @@ func Test_Importer(t *testing.T) { assert.Equal(t, op.(*bug.EditCommentOperation).Author.Name(), ops[i].(*bug.EditCommentOperation).Author.Name()) default: - panic("Unknown operation type") + panic("unknown operation type") } } }) From 07492fb72a4af565d607432c681143d77f102515 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Fri, 21 Jun 2019 23:25:23 +0200 Subject: [PATCH 11/29] [bridge/github] importer: tag imported issues with origin metadata [bridge/github] exporter: correct export signature and cache maps --- bridge/github/export.go | 26 +++++++++++++------------- bridge/github/github.go | 2 +- bridge/github/import.go | 3 +++ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index e761136065b919f63e19de18748b9adbb2068b61..658ef4849c42b579fabea41dc2e66660164f89cf 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -15,7 +15,6 @@ import ( "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" - "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/util/git" ) @@ -23,11 +22,11 @@ import ( type githubExporter struct { conf core.Configuration - // a map containing - tokens map[string]string + // cache identities clients + identityClient map[string]*githubv4.Client - // clients - clients map[string]*githubv4.Client + // map identity with their tokens + identityToken map[string]string // github repository ID repositoryID string @@ -44,20 +43,20 @@ type githubExporter struct { func (ge *githubExporter) Init(conf core.Configuration) error { //TODO: initialize with multiple tokens ge.conf = conf - ge.tokens = make(map[string]string) - ge.clients = make(map[string]*githubv4.Client) + ge.identityToken = make(map[string]string) + ge.identityClient = make(map[string]*githubv4.Client) ge.cachedIDs = make(map[string]string) ge.cachedLabels = make(map[string]string) return nil } func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) { - client, ok := ge.clients[id] + client, ok := ge.identityClient[id] if ok { return client, nil } - token, ok := ge.tokens[id] + token, ok := ge.identityToken[id] if !ok { return nil, fmt.Errorf("unknown identity token") } @@ -72,7 +71,7 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) erro return err } - ge.tokens[user.Id()] = ge.conf[keyToken] + ge.identityToken[user.Id()] = ge.conf[keyToken] // get repository node id ge.repositoryID, err = getRepositoryNodeID( @@ -102,10 +101,10 @@ bugLoop: // if identity participated in a bug for _, p := range snapshot.Participants { - for userId := range ge.tokens { + for userId := range ge.identityToken { if p.Id() == userId { // try to export the bug and it associated events - if err := ge.exportBug(b, user.Identity, since); err != nil { + if err := ge.exportBug(b, since); err != nil { return err } @@ -119,7 +118,7 @@ bugLoop: } // exportBug publish bugs and related events -func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, since time.Time) error { +func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { snapshot := b.Snapshot() var bugGithubID string @@ -148,6 +147,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, user identity.Interface, bugGithubURL = githubURL } else { + // check that we have a token for operation author client, err := ge.getIdentityClient(author.Id()) if err != nil { // if bug is still not exported and user cannot author bug stop the execution diff --git a/bridge/github/github.go b/bridge/github/github.go index 46004dc85757d89c92adf303ce2811810c5bdafe..176bdd84c1c02299d6617ab61732a9a66c77f1f3 100644 --- a/bridge/github/github.go +++ b/bridge/github/github.go @@ -17,7 +17,7 @@ func init() { type Github struct{} func (*Github) Target() string { - return "github" + return target } func (*Github) NewImporter() core.Importer { diff --git a/bridge/github/import.go b/bridge/github/import.go index 6cfd022ee0afb03eea2186ec99a7928624a55a2b..d07f04991e1e2ea10410790f620e241d502b3490 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -16,6 +16,7 @@ import ( ) const ( + keyOrigin = "origin" keyGithubId = "github-id" keyGithubUrl = "github-url" keyGithubLogin = "github-login" @@ -113,6 +114,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline cleanText, nil, map[string]string{ + keyOrigin: target, keyGithubId: parseId(issue.Id), keyGithubUrl: issue.Url.String(), }) @@ -147,6 +149,7 @@ func (gi *githubImporter) ensureIssue(repo *cache.RepoCache, issue issueTimeline cleanText, nil, map[string]string{ + keyOrigin: target, keyGithubId: parseId(issue.Id), keyGithubUrl: issue.Url.String(), }, From a58ac69c1f9246a1e544f0032f8dee8e8a769b04 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 22 Jun 2019 13:06:51 +0200 Subject: [PATCH 12/29] [bridge/github] exporter: Check bug import origin [bridge/github] export only allowed bugs --- bridge/github/export.go | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index 658ef4849c42b579fabea41dc2e66660164f89cf..71790031c7c16a1c5409b79a4de0e59a931c273f 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -22,6 +22,9 @@ import ( type githubExporter struct { conf core.Configuration + // export only bugs taged with one of these origins + onlyOrigins []string + // cache identities clients identityClient map[string]*githubv4.Client @@ -41,8 +44,8 @@ type githubExporter struct { // Init . func (ge *githubExporter) Init(conf core.Configuration) error { - //TODO: initialize with multiple tokens ge.conf = conf + //TODO: initialize with multiple tokens ge.identityToken = make(map[string]string) ge.identityClient = make(map[string]*githubv4.Client) ge.cachedIDs = make(map[string]string) @@ -50,18 +53,38 @@ func (ge *githubExporter) Init(conf core.Configuration) error { return nil } +func (ge *githubExporter) allowOrigin(origin string) bool { + if ge.onlyOrigins == nil { + return true + } + + for _, o := range ge.onlyOrigins { + if origin == o { + return true + } + } + + return false +} + func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) { client, ok := ge.identityClient[id] if ok { return client, nil } + // get token token, ok := ge.identityToken[id] if !ok { return nil, fmt.Errorf("unknown identity token") } - return buildClient(token), nil + // create client + client = buildClient(token) + // cache client + ge.identityClient[id] = client + + return client, nil } // ExportAll export all event made by the current user to Github @@ -99,8 +122,8 @@ bugLoop: continue } - // if identity participated in a bug for _, p := range snapshot.Participants { + // if we have a token for one of the participants for userId := range ge.identityToken { if p.Id() == userId { // try to export the bug and it associated events @@ -133,6 +156,13 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { createOp := snapshot.Operations[0].(*bug.CreateOperation) author := createOp.GetAuthor() + // skip bug if origin is not allowed + origin, ok := createOp.GetMetadata(keyOrigin) + if ok && !ge.allowOrigin(origin) { + // TODO print a warn ? + return nil + } + // get github bug ID githubID, ok := createOp.GetMetadata(keyGithubId) if ok { From e511f3d885e5e7f15d3b7f8e500380f0c63bc377 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 22 Jun 2019 13:40:17 +0200 Subject: [PATCH 13/29] [misc] update man docs and completion scripts [cache] BugCache: Fix set metadata raw [bridge/github] Fix graphql input objects --- bridge/github/export.go | 27 +++++++++++-------- cache/bug_cache.go | 2 +- doc/man/git-bug-bridge-push.1 | 29 +++++++++++++++++++++ doc/man/git-bug-bridge.1 | 2 +- doc/md/git-bug_bridge.md | 1 + doc/md/git-bug_bridge_push.md | 22 ++++++++++++++++ misc/bash_completion/git-bug | 21 +++++++++++++++ misc/zsh_completion/git-bug | 49 +++++++++++++++++++++++++++++++++++ 8 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 doc/man/git-bug-bridge-push.1 create mode 100644 doc/md/git-bug_bridge_push.md diff --git a/bridge/github/export.go b/bridge/github/export.go index 71790031c7c16a1c5409b79a4de0e59a931c273f..38df9caf060fc07aee9fb7067b6cba9326e09858 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -22,6 +22,9 @@ import ( type githubExporter struct { conf core.Configuration + // number of exported bugs + exportedBugs int + // export only bugs taged with one of these origins onlyOrigins []string @@ -166,12 +169,11 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // get github bug ID githubID, ok := createOp.GetMetadata(keyGithubId) if ok { - githubURL, ok := createOp.GetMetadata(keyGithubId) + githubURL, ok := createOp.GetMetadata(keyGithubUrl) if !ok { // if we find github ID, github URL must be found too panic("expected to find github issue URL") } - // will be used to mark operation related to a bug as exported bugGithubID = githubID bugGithubURL = githubURL @@ -191,7 +193,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // create bug id, url, err := createGithubIssue(client, ge.repositoryID, createOp.Title, createOp.Message) if err != nil { - return fmt.Errorf("creating exporting github issue %v", err) + return fmt.Errorf("exporting github issue: %v", err) } hash, err := createOp.Hash() @@ -263,11 +265,13 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { } case *bug.EditCommentOperation: + opr := op.(*bug.EditCommentOperation) targetHash := opr.Target.String() // Since github doesn't consider the issue body as a comment if targetHash == bugCreationHash { + // case bug creation operation: we need to edit the Github issue if err := updateGithubIssueBody(client, bugGithubID, opr.Message); err != nil { return fmt.Errorf("editing issue: %v", err) @@ -277,6 +281,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { url = bugGithubURL } else { + // case comment edition operation: we need to edit the Github comment commentID, ok := ge.cachedIDs[targetHash] if !ok { @@ -552,7 +557,7 @@ func (ge *githubExporter) getLabelsIDs(repositoryID string, labels []bug.Label) // create a github issue and return it ID func createGithubIssue(gc *githubv4.Client, repositoryID, title, body string) (string, string, error) { m := &createIssueMutation{} - input := &githubv4.CreateIssueInput{ + input := githubv4.CreateIssueInput{ RepositoryID: repositoryID, Title: githubv4.String(title), Body: (*githubv4.String)(&body), @@ -569,7 +574,7 @@ func createGithubIssue(gc *githubv4.Client, repositoryID, title, body string) (s // add a comment to an issue and return it ID func addCommentGithubIssue(gc *githubv4.Client, subjectID string, body string) (string, string, error) { m := &addCommentToIssueMutation{} - input := &githubv4.AddCommentInput{ + input := githubv4.AddCommentInput{ SubjectID: subjectID, Body: githubv4.String(body), } @@ -584,7 +589,7 @@ func addCommentGithubIssue(gc *githubv4.Client, subjectID string, body string) ( func editCommentGithubIssue(gc *githubv4.Client, commentID, body string) (string, string, error) { m := &updateIssueCommentMutation{} - input := &githubv4.UpdateIssueCommentInput{ + input := githubv4.UpdateIssueCommentInput{ ID: commentID, Body: githubv4.String(body), } @@ -606,7 +611,7 @@ func updateGithubIssueStatus(gc *githubv4.Client, id string, status bug.Status) state = githubv4.IssueStateOpen } - input := &githubv4.UpdateIssueInput{ + input := githubv4.UpdateIssueInput{ ID: id, State: &state, } @@ -620,7 +625,7 @@ func updateGithubIssueStatus(gc *githubv4.Client, id string, status bug.Status) func updateGithubIssueBody(gc *githubv4.Client, id string, body string) error { m := &updateIssueMutation{} - input := &githubv4.UpdateIssueInput{ + input := githubv4.UpdateIssueInput{ ID: id, Body: (*githubv4.String)(&body), } @@ -634,7 +639,7 @@ func updateGithubIssueBody(gc *githubv4.Client, id string, body string) error { func updateGithubIssueTitle(gc *githubv4.Client, id, title string) error { m := &updateIssueMutation{} - input := &githubv4.UpdateIssueInput{ + input := githubv4.UpdateIssueInput{ ID: id, Title: (*githubv4.String)(&title), } @@ -654,7 +659,7 @@ func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelable } m := &addLabelsToLabelableMutation{} - inputAdd := &githubv4.AddLabelsToLabelableInput{ + inputAdd := githubv4.AddLabelsToLabelableInput{ LabelableID: labelableID, LabelIDs: addedIDs, } @@ -670,7 +675,7 @@ func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelable } m2 := &removeLabelsFromLabelableMutation{} - inputRemove := &githubv4.RemoveLabelsFromLabelableInput{ + inputRemove := githubv4.RemoveLabelsFromLabelableInput{ LabelableID: labelableID, LabelIDs: removedIDs, } diff --git a/cache/bug_cache.go b/cache/bug_cache.go index 36f2139edb1f0c8929ff00f12d243ab159e47d1c..758fb0b7feefc1560a0f35e94e5cacfef924bb7c 100644 --- a/cache/bug_cache.go +++ b/cache/bug_cache.go @@ -260,7 +260,7 @@ func (c *BugCache) SetMetadata(target git.Hash, newMetadata map[string]string) ( return nil, err } - return c.SetMetadataRaw(author, time.Now().Unix(), target, nil) + return c.SetMetadataRaw(author, time.Now().Unix(), target, newMetadata) } func (c *BugCache) SetMetadataRaw(author *IdentityCache, unixTime int64, target git.Hash, newMetadata map[string]string) (*bug.SetMetadataOperation, error) { diff --git a/doc/man/git-bug-bridge-push.1 b/doc/man/git-bug-bridge-push.1 new file mode 100644 index 0000000000000000000000000000000000000000..1257781b55596cbccb770910ed0e92a02c023667 --- /dev/null +++ b/doc/man/git-bug-bridge-push.1 @@ -0,0 +1,29 @@ +.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" "" +.nh +.ad l + + +.SH NAME +.PP +git\-bug\-bridge\-push \- Push updates. + + +.SH SYNOPSIS +.PP +\fBgit\-bug bridge push [] [flags]\fP + + +.SH DESCRIPTION +.PP +Push updates. + + +.SH OPTIONS +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for push + + +.SH SEE ALSO +.PP +\fBgit\-bug\-bridge(1)\fP diff --git a/doc/man/git-bug-bridge.1 b/doc/man/git-bug-bridge.1 index 9c198716a0784b68b0a26532f10fd912ccf4a342..dfede4e03b87dbc04cf87d29491b98632e896c63 100644 --- a/doc/man/git-bug-bridge.1 +++ b/doc/man/git-bug-bridge.1 @@ -26,4 +26,4 @@ Configure and use bridges to other bug trackers. .SH SEE ALSO .PP -\fBgit\-bug(1)\fP, \fBgit\-bug\-bridge\-configure(1)\fP, \fBgit\-bug\-bridge\-pull(1)\fP, \fBgit\-bug\-bridge\-rm(1)\fP +\fBgit\-bug(1)\fP, \fBgit\-bug\-bridge\-configure(1)\fP, \fBgit\-bug\-bridge\-pull(1)\fP, \fBgit\-bug\-bridge\-push(1)\fP, \fBgit\-bug\-bridge\-rm(1)\fP diff --git a/doc/md/git-bug_bridge.md b/doc/md/git-bug_bridge.md index 7d01a300866be3efba9e9c33d9b4d87214559ac3..dfd61e294752c0fa5e23ff107137ae8ee14cf79a 100644 --- a/doc/md/git-bug_bridge.md +++ b/doc/md/git-bug_bridge.md @@ -21,5 +21,6 @@ git-bug bridge [flags] * [git-bug](git-bug.md) - A bug tracker embedded in Git. * [git-bug bridge configure](git-bug_bridge_configure.md) - Configure a new bridge. * [git-bug bridge pull](git-bug_bridge_pull.md) - Pull updates. +* [git-bug bridge push](git-bug_bridge_push.md) - Push updates. * [git-bug bridge rm](git-bug_bridge_rm.md) - Delete a configured bridge. diff --git a/doc/md/git-bug_bridge_push.md b/doc/md/git-bug_bridge_push.md new file mode 100644 index 0000000000000000000000000000000000000000..35436dd66b592b70fdc52cfdd7717795e3138d63 --- /dev/null +++ b/doc/md/git-bug_bridge_push.md @@ -0,0 +1,22 @@ +## git-bug bridge push + +Push updates. + +### Synopsis + +Push updates. + +``` +git-bug bridge push [] [flags] +``` + +### Options + +``` + -h, --help help for push +``` + +### SEE ALSO + +* [git-bug bridge](git-bug_bridge.md) - Configure and use bridges to other bug trackers. + diff --git a/misc/bash_completion/git-bug b/misc/bash_completion/git-bug index 741dcc49d203d627010e2707d9696eadf8f7dd02..ae3242e57c08b961ddf8e0d9d218a6f067abe1de 100644 --- a/misc/bash_completion/git-bug +++ b/misc/bash_completion/git-bug @@ -351,6 +351,26 @@ _git-bug_bridge_pull() noun_aliases=() } +_git-bug_bridge_push() +{ + last_command="git-bug_bridge_push" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _git-bug_bridge_rm() { last_command="git-bug_bridge_rm" @@ -380,6 +400,7 @@ _git-bug_bridge() commands=() commands+=("configure") commands+=("pull") + commands+=("push") commands+=("rm") flags=() diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index ed676724b608cd04e019203dd4f1b59bc56e35b7..97fb27c58831586717620e7229a1bc90f9bccaf3 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -1,3 +1,4 @@ +<<<<<<< HEAD #compdef _git-bug git-bug @@ -394,3 +395,51 @@ function _git-bug_webui { '(-p --port)'{-p,--port}'[Port to listen to (default is random)]:' } +======= +#compdef git-bug + +_arguments \ + '1: :->level1' \ + '2: :->level2' \ + '3: :_files' +case $state in + level1) + case $words[1] in + git-bug) + _arguments '1: :(add bridge commands comment deselect label ls ls-id ls-label pull push select show status termui title user version webui)' + ;; + *) + _arguments '*: :_files' + ;; + esac + ;; + level2) + case $words[2] in + bridge) + _arguments '2: :(configure pull push rm)' + ;; + comment) + _arguments '2: :(add)' + ;; + label) + _arguments '2: :(add rm)' + ;; + status) + _arguments '2: :(close open)' + ;; + title) + _arguments '2: :(edit)' + ;; + user) + _arguments '2: :(adopt create ls)' + ;; + *) + _arguments '*: :_files' + ;; + esac + ;; + *) + _arguments '*: :_files' + ;; +esac +>>>>>>> 3daac46... update man docs and completion scripts From f70c279c1e8dc7f73e08679f8789c35c1dfdd59d Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 22 Jun 2019 17:10:23 +0200 Subject: [PATCH 14/29] [bridge/github] exporter: Improve error handling [bridge/github] queries: use api v4 for getLabel / createLabel [bridge/github] add comments to getIdentityClient --- bridge/github/export.go | 139 ++++++++++++++++------------------ bridge/github/export_query.go | 17 +++++ bridge/github/import_query.go | 8 ++ 3 files changed, 92 insertions(+), 72 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index 38df9caf060fc07aee9fb7067b6cba9326e09858..a41efc99fa12adafe327e0a02ce98ffcc83fae95 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -10,6 +10,7 @@ import ( "net/http" "time" + "github.com/pkg/errors" "github.com/shurcooL/githubv4" "github.com/MichaelMure/git-bug/bridge/core" @@ -18,6 +19,10 @@ import ( "github.com/MichaelMure/git-bug/util/git" ) +var ( + ErrMissingIdentityToken = errors.New("missing identity token") +) + // githubImporter implement the Importer interface type githubExporter struct { conf core.Configuration @@ -70,6 +75,8 @@ func (ge *githubExporter) allowOrigin(origin string) bool { return false } +// getIdentityClient return an identity github api v4 client +// if no client were found it will initilize it from the known tokens map and cache it for next it use func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) { client, ok := ge.identityClient[id] if ok { @@ -79,7 +86,7 @@ func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) // get token token, ok := ge.identityToken[id] if !ok { - return nil, fmt.Errorf("unknown identity token") + return nil, ErrMissingIdentityToken } // create client @@ -182,28 +189,32 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // check that we have a token for operation author client, err := ge.getIdentityClient(author.Id()) if err != nil { - // if bug is still not exported and user cannot author bug stop the execution + // if bug is still not exported and we do not have the author stop the execution - // TODO: maybe print a warning ? - // this is not an error - // don't export bug + fmt.Println("warning: skipping issue due to missing token for bug creator") + // this is not an error, don't export bug return nil } // create bug id, url, err := createGithubIssue(client, ge.repositoryID, createOp.Title, createOp.Message) if err != nil { - return fmt.Errorf("exporting github issue: %v", err) + return errors.Wrap(err, "exporting github issue") } hash, err := createOp.Hash() if err != nil { - return fmt.Errorf("comment hash: %v", err) + return errors.Wrap(err, "comment hash") } // mark bug creation operation as exported if err := markOperationAsExported(b, hash, id, url); err != nil { - return fmt.Errorf("marking operation as exported: %v", err) + return errors.Wrap(err, "marking operation as exported") + } + + // commit operation to avoid creating multiple issues with multiple pushes + if err := b.CommitAsNeeded(); err != nil { + return errors.Wrap(err, "bug commit") } // cache bug github ID and URL @@ -231,7 +242,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // get operation hash hash, err := op.Hash() if err != nil { - return fmt.Errorf("reading operation hash: %v", err) + return errors.Wrap(err, "operation hash") } // ignore imported (or exported) operations from github @@ -256,12 +267,12 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // send operation to github id, url, err = addCommentGithubIssue(client, bugGithubID, opr.Message) if err != nil { - return fmt.Errorf("adding comment: %v", err) + return errors.Wrap(err, "adding comment") } hash, err = opr.Hash() if err != nil { - return fmt.Errorf("comment hash: %v", err) + return errors.Wrap(err, "comment hash") } case *bug.EditCommentOperation: @@ -274,7 +285,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // case bug creation operation: we need to edit the Github issue if err := updateGithubIssueBody(client, bugGithubID, opr.Message); err != nil { - return fmt.Errorf("editing issue: %v", err) + return errors.Wrap(err, "editing issue") } id = bugGithubID @@ -290,7 +301,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { eid, eurl, err := editCommentGithubIssue(client, commentID, opr.Message) if err != nil { - return fmt.Errorf("editing comment: %v", err) + return errors.Wrap(err, "editing comment") } // use comment id/url instead of issue id/url @@ -300,18 +311,18 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { hash, err = opr.Hash() if err != nil { - return fmt.Errorf("comment hash: %v", err) + return errors.Wrap(err, "comment hash") } case *bug.SetStatusOperation: opr := op.(*bug.SetStatusOperation) if err := updateGithubIssueStatus(client, bugGithubID, opr.Status); err != nil { - return fmt.Errorf("updating status %v: %v", bugGithubID, err) + return errors.Wrap(err, "editing status") } hash, err = opr.Hash() if err != nil { - return fmt.Errorf("comment hash: %v", err) + return errors.Wrap(err, "set status operation hash") } id = bugGithubID @@ -320,12 +331,12 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { case *bug.SetTitleOperation: opr := op.(*bug.SetTitleOperation) if err := updateGithubIssueTitle(client, bugGithubID, opr.Title); err != nil { - return fmt.Errorf("editing comment %v: %v", bugGithubID, err) + return errors.Wrap(err, "editing title") } hash, err = opr.Hash() if err != nil { - return fmt.Errorf("comment hash: %v", err) + return errors.Wrap(err, "set title operation hash") } id = bugGithubID @@ -334,12 +345,12 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { case *bug.LabelChangeOperation: opr := op.(*bug.LabelChangeOperation) if err := ge.updateGithubIssueLabels(client, bugGithubID, opr.Added, opr.Removed); err != nil { - return fmt.Errorf("updating labels %v: %v", bugGithubID, err) + return errors.Wrap(err, "updating labels") } hash, err = opr.Hash() if err != nil { - return fmt.Errorf("comment hash: %v", err) + return errors.Wrap(err, "label change operation hash") } id = bugGithubID @@ -351,12 +362,12 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // mark operation as exported if err := markOperationAsExported(b, hash, id, url); err != nil { - return fmt.Errorf("marking operation as exported: %v", err) + return errors.Wrap(err, "marking operation as exported") } - } - if err := b.CommitAsNeeded(); err != nil { - return fmt.Errorf("bug commit: %v", err) + if err := b.CommitAsNeeded(); err != nil { + return errors.Wrap(err, "bug commit") + } } return nil @@ -415,49 +426,17 @@ func markOperationAsExported(b *cache.BugCache, target git.Hash, githubID, githu } // get label from github -func (ge *githubExporter) getGithubLabelID(label string) (string, error) { - url := fmt.Sprintf("%s/repos/%s/%s/labels/%s", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject], label) - - client := &http.Client{ - Timeout: defaultTimeout, - } - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return "", err - } - - // need the token for private repositories - req.Header.Set("Authorization", fmt.Sprintf("token %s", ge.conf[keyToken])) - - resp, err := client.Do(req) - if err != nil { - return "", err - } - - if resp.StatusCode != http.StatusFound { - return "", fmt.Errorf("error getting label: status code: %v", resp.StatusCode) - } - - aux := struct { - ID string `json:"id"` - NodeID string `json:"node_id"` - Color string `json:"color"` - }{} - - data, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - err = json.Unmarshal(data, &aux) - if err != nil { +func (ge *githubExporter) getGithubLabelID(gc *githubv4.Client, label string) (string, error) { + q := labelQuery{} + variables := map[string]interface{}{"name": label} + if err := gc.Query(context.TODO(), &q, variables); err != nil { return "", err } - return aux.NodeID, nil + return q.Repository.Label.ID, nil } -// create github label using api v3 -func (ge *githubExporter) createGithubLabel(label, labelColor string) (string, error) { +func (ge *githubExporter) createGithubLabel(gc *githubv4.Client, label, labelColor string) (string, error) { url := fmt.Sprintf("%s/repos/%s/%s/labels", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject]) client := &http.Client{ @@ -498,6 +477,22 @@ func (ge *githubExporter) createGithubLabel(label, labelColor string) (string, e return aux.NodeID, nil } +// create github label using api v4 +func (ge *githubExporter) createGithubLabelV4(gc *githubv4.Client, label, labelColor string) (string, error) { + m := &createLabelMutation{} + input := createLabelInput{ + RepositoryID: ge.repositoryID, + Name: githubv4.String(label), + Color: githubv4.String(labelColor), + } + + if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { + return "", err + } + + return m.CreateLabel.Label.ID, nil +} + // randomHexColor return a random hex color code func randomHexColor() string { bytes := make([]byte, 6) @@ -508,9 +503,9 @@ func randomHexColor() string { return hex.EncodeToString(bytes) } -func (ge *githubExporter) getOrCreateGithubLabelID(repositoryID, label string) (string, error) { +func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, repositoryID, label string) (string, error) { // try to get label id - labelID, err := ge.getGithubLabelID(label) + labelID, err := ge.getGithubLabelID(gc, label) if err == nil { return labelID, nil } @@ -520,7 +515,7 @@ func (ge *githubExporter) getOrCreateGithubLabelID(repositoryID, label string) ( color := randomHexColor() // create label and return id - labelID, err = ge.createGithubLabel(label, color) + labelID, err = ge.createGithubLabel(gc, label, color) if err != nil { return "", err } @@ -528,7 +523,7 @@ func (ge *githubExporter) getOrCreateGithubLabelID(repositoryID, label string) ( return labelID, nil } -func (ge *githubExporter) getLabelsIDs(repositoryID string, labels []bug.Label) ([]githubv4.ID, error) { +func (ge *githubExporter) getLabelsIDs(gc *githubv4.Client, repositoryID string, labels []bug.Label) ([]githubv4.ID, error) { ids := make([]githubv4.ID, 0, len(labels)) var err error @@ -539,9 +534,9 @@ func (ge *githubExporter) getLabelsIDs(repositoryID string, labels []bug.Label) id, ok := ge.cachedLabels[label] if !ok { // try to query label id - id, err = ge.getOrCreateGithubLabelID(repositoryID, label) + id, err = ge.getOrCreateGithubLabelID(gc, repositoryID, label) if err != nil { - return nil, fmt.Errorf("get or create github label: %v", err) + return nil, errors.Wrap(err, "get or create github label") } // cache label id @@ -653,9 +648,9 @@ func updateGithubIssueTitle(gc *githubv4.Client, id, title string) error { // update github issue labels func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelableID string, added, removed []bug.Label) error { - addedIDs, err := ge.getLabelsIDs(labelableID, added) + addedIDs, err := ge.getLabelsIDs(gc, labelableID, added) if err != nil { - return fmt.Errorf("getting added labels ids: %v", err) + return errors.Wrap(err, "getting added labels ids") } m := &addLabelsToLabelableMutation{} @@ -669,9 +664,9 @@ func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelable return err } - removedIDs, err := ge.getLabelsIDs(labelableID, added) + removedIDs, err := ge.getLabelsIDs(gc, labelableID, added) if err != nil { - return fmt.Errorf("getting added labels ids: %v", err) + return errors.Wrap(err, "getting added labels ids") } m2 := &removeLabelsFromLabelableMutation{} diff --git a/bridge/github/export_query.go b/bridge/github/export_query.go index a527399e61ee8eb9f685e6f080dfc27bee7d1d87..006d251126588cc7996b0996de47e8e511834499 100644 --- a/bridge/github/export_query.go +++ b/bridge/github/export_query.go @@ -1,5 +1,7 @@ package github +import "github.com/shurcooL/githubv4" + type createIssueMutation struct { CreateIssue struct { Issue struct { @@ -36,6 +38,14 @@ type updateIssueCommentMutation struct { } `graphql:"updateIssueComment(input:$input)"` } +type createLabelMutation struct { + CreateLabel struct { + Label struct { + ID string `graphql:"id"` + } `graphql:"label"` + } `graphql:"createLabel(input:{repositoryId: $repositoryId, name: $name, color: $color})"` +} + type removeLabelsFromLabelableMutation struct { AddLabels struct{} `graphql:"removeLabelsFromLabelable(input:$input)"` } @@ -43,3 +53,10 @@ type removeLabelsFromLabelableMutation struct { type addLabelsToLabelableMutation struct { RemoveLabels struct{} `graphql:"addLabelsToLabelable(input:$input)"` } + +type createLabelInput struct { + Color githubv4.String `json:"color"` + Description *githubv4.String `json:"description"` + Name githubv4.String `json:"name"` + RepositoryID githubv4.ID `json:"repositoryId"` +} diff --git a/bridge/github/import_query.go b/bridge/github/import_query.go index 4d5886f69235215a501bef707d857820c73d2b97..6c0db5e14da14e1e0525980ae13da393de5db52e 100644 --- a/bridge/github/import_query.go +++ b/bridge/github/import_query.go @@ -168,3 +168,11 @@ type userQuery struct { Email githubv4.String } `graphql:"user(login: $login)"` } + +type labelQuery struct { + Repository struct { + Label struct { + ID string `graphql:"id"` + } `graphql:"label(name: $name)"` + } `graphql:"repository(owner: $owner, name: $name)"` +} From 4ad3d336c69922a7e34c782e995db9f1adcbc403 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 22 Jun 2019 19:46:42 +0200 Subject: [PATCH 15/29] [bridge/github] use context.WithTimeout on all graphql queries --- bridge/github/export.go | 62 ++++++++++++++++++++++++++++++++------- bridge/github/import.go | 6 +++- bridge/github/iterator.go | 25 +++++++++++++--- 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index a41efc99fa12adafe327e0a02ce98ffcc83fae95..e76e95f068804556b8283b72e640bff7101ec3cf 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -429,7 +429,12 @@ func markOperationAsExported(b *cache.BugCache, target git.Hash, githubID, githu func (ge *githubExporter) getGithubLabelID(gc *githubv4.Client, label string) (string, error) { q := labelQuery{} variables := map[string]interface{}{"name": label} - if err := gc.Query(context.TODO(), &q, variables); err != nil { + + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := gc.Query(ctx, &q, variables); err != nil { return "", err } @@ -486,7 +491,11 @@ func (ge *githubExporter) createGithubLabelV4(gc *githubv4.Client, label, labelC Color: githubv4.String(labelColor), } - if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := gc.Mutate(ctx, m, input, nil); err != nil { return "", err } @@ -558,7 +567,11 @@ func createGithubIssue(gc *githubv4.Client, repositoryID, title, body string) (s Body: (*githubv4.String)(&body), } - if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := gc.Mutate(ctx, m, input, nil); err != nil { return "", "", err } @@ -574,7 +587,11 @@ func addCommentGithubIssue(gc *githubv4.Client, subjectID string, body string) ( Body: githubv4.String(body), } - if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := gc.Mutate(ctx, m, input, nil); err != nil { return "", "", err } @@ -589,7 +606,11 @@ func editCommentGithubIssue(gc *githubv4.Client, commentID, body string) (string Body: githubv4.String(body), } - if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := gc.Mutate(ctx, m, input, nil); err != nil { return "", "", err } @@ -611,7 +632,11 @@ func updateGithubIssueStatus(gc *githubv4.Client, id string, status bug.Status) State: &state, } - if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := gc.Mutate(ctx, m, input, nil); err != nil { return err } @@ -625,7 +650,11 @@ func updateGithubIssueBody(gc *githubv4.Client, id string, body string) error { Body: (*githubv4.String)(&body), } - if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := gc.Mutate(ctx, m, input, nil); err != nil { return err } @@ -639,7 +668,11 @@ func updateGithubIssueTitle(gc *githubv4.Client, id, title string) error { Title: (*githubv4.String)(&title), } - if err := gc.Mutate(context.TODO(), m, input, nil); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := gc.Mutate(ctx, m, input, nil); err != nil { return err } @@ -648,6 +681,7 @@ func updateGithubIssueTitle(gc *githubv4.Client, id, title string) error { // update github issue labels func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelableID string, added, removed []bug.Label) error { + addedIDs, err := ge.getLabelsIDs(gc, labelableID, added) if err != nil { return errors.Wrap(err, "getting added labels ids") @@ -659,11 +693,16 @@ func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelable LabelIDs: addedIDs, } + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + // add labels - if err := gc.Mutate(context.TODO(), m, inputAdd, nil); err != nil { + if err := gc.Mutate(ctx, m, inputAdd, nil); err != nil { + cancel() return err } + cancel() removedIDs, err := ge.getLabelsIDs(gc, labelableID, added) if err != nil { return errors.Wrap(err, "getting added labels ids") @@ -675,8 +714,11 @@ func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelable LabelIDs: removedIDs, } + ctx2, cancel2 := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel2() + // remove label labels - if err := gc.Mutate(context.TODO(), m2, inputRemove, nil); err != nil { + if err := gc.Mutate(ctx2, m2, inputRemove, nil); err != nil { return err } diff --git a/bridge/github/import.go b/bridge/github/import.go index d07f04991e1e2ea10410790f620e241d502b3490..e87c6b4b04be8919c414b8e9ef82cbc99288510b 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -513,7 +513,11 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, gc := buildClient(gi.conf[keyToken]) - err = gc.Query(context.TODO(), &q, variables) + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + err = gc.Query(ctx, &q, variables) if err != nil { return nil, err } diff --git a/bridge/github/iterator.go b/bridge/github/iterator.go index 3620f50d8030601d087173246def02682122e0d8..6d63cf42ff054ba676d0c069f6b06c245b7d725d 100644 --- a/bridge/github/iterator.go +++ b/bridge/github/iterator.go @@ -147,7 +147,11 @@ func (i *iterator) Error() error { } func (i *iterator) queryIssue() bool { - if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil { i.err = err return false } @@ -220,7 +224,12 @@ func (i *iterator) NextTimelineItem() bool { // more timelines, query them i.timeline.variables["timelineAfter"] = i.timeline.query.Repository.Issues.Nodes[0].Timeline.PageInfo.EndCursor - if err := i.gc.Query(context.TODO(), &i.timeline.query, i.timeline.variables); err != nil { + + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := i.gc.Query(ctx, &i.timeline.query, i.timeline.variables); err != nil { i.err = err return false } @@ -236,7 +245,11 @@ func (i *iterator) TimelineItemValue() timelineItem { } func (i *iterator) queryIssueEdit() bool { - if err := i.gc.Query(context.TODO(), &i.issueEdit.query, i.issueEdit.variables); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := i.gc.Query(ctx, &i.issueEdit.query, i.issueEdit.variables); err != nil { i.err = err //i.timeline.issueEdit.index = -1 return false @@ -334,7 +347,11 @@ func (i *iterator) IssueEditValue() userContentEdit { } func (i *iterator) queryCommentEdit() bool { - if err := i.gc.Query(context.TODO(), &i.commentEdit.query, i.commentEdit.variables); err != nil { + parentCtx := context.Background() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + if err := i.gc.Query(ctx, &i.commentEdit.query, i.commentEdit.variables); err != nil { i.err = err return false } From 23f2c58e920ea239842e0303af0b5bba5e1d3665 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 22 Jun 2019 20:08:51 +0200 Subject: [PATCH 16/29] [bridge/github] create label: Generate hexColor from label rgba --- bridge/github/export.go | 58 +++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index e76e95f068804556b8283b72e640bff7101ec3cf..df8400daf1438b3d6dc330c7d4ce162a1b48c005 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -1,12 +1,11 @@ package github import ( + "bytes" "context" - "encoding/hex" "encoding/json" "fmt" "io/ioutil" - "math/rand" "net/http" "time" @@ -448,7 +447,21 @@ func (ge *githubExporter) createGithubLabel(gc *githubv4.Client, label, labelCol Timeout: defaultTimeout, } - req, err := http.NewRequest("POST", url, nil) + params := struct { + Name string `json:"name"` + Color string `json:"color"` + Description string `json:"description"` + }{ + Name: label, + Color: labelColor, + } + + data, err := json.Marshal(params) + if err != nil { + return "", err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) if err != nil { return "", err } @@ -462,16 +475,18 @@ func (ge *githubExporter) createGithubLabel(gc *githubv4.Client, label, labelCol } if resp.StatusCode != http.StatusCreated { + //d, _ := ioutil.ReadAll(resp.Body) + //fmt.Println(string(d)) return "", fmt.Errorf("error creating label: response status %v", resp.StatusCode) } aux := struct { - ID string `json:"id"` + ID int `json:"id"` NodeID string `json:"node_id"` Color string `json:"color"` }{} - data, _ := ioutil.ReadAll(resp.Body) + data, _ = ioutil.ReadAll(resp.Body) defer resp.Body.Close() err = json.Unmarshal(data, &aux) @@ -479,6 +494,7 @@ func (ge *githubExporter) createGithubLabel(gc *githubv4.Client, label, labelCol return "", err } + fmt.Println("ezzz", aux.NodeID) return aux.NodeID, nil } @@ -502,29 +518,21 @@ func (ge *githubExporter) createGithubLabelV4(gc *githubv4.Client, label, labelC return m.CreateLabel.Label.ID, nil } -// randomHexColor return a random hex color code -func randomHexColor() string { - bytes := make([]byte, 6) - if _, err := rand.Read(bytes); err != nil { - return "fffff" - } - - return hex.EncodeToString(bytes) -} - -func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, repositoryID, label string) (string, error) { +func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, repositoryID string, label bug.Label) (string, error) { // try to get label id - labelID, err := ge.getGithubLabelID(gc, label) + labelID, err := ge.getGithubLabelID(gc, string(label)) if err == nil { return labelID, nil } - // random color - //TODO: no random - color := randomHexColor() + // hex color + rgba := label.RGBA() + hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) + + fmt.Println("creating color", label, hexColor) // create label and return id - labelID, err = ge.createGithubLabel(gc, label, color) + labelID, err = ge.createGithubLabel(gc, string(label), hexColor) if err != nil { return "", err } @@ -537,10 +545,8 @@ func (ge *githubExporter) getLabelsIDs(gc *githubv4.Client, repositoryID string, var err error // check labels ids - for _, l := range labels { - label := string(l) - - id, ok := ge.cachedLabels[label] + for _, label := range labels { + id, ok := ge.cachedLabels[string(label)] if !ok { // try to query label id id, err = ge.getOrCreateGithubLabelID(gc, repositoryID, label) @@ -549,7 +555,7 @@ func (ge *githubExporter) getLabelsIDs(gc *githubv4.Client, repositoryID string, } // cache label id - ge.cachedLabels[label] = id + ge.cachedLabels[string(label)] = id } ids = append(ids, githubv4.ID(id)) From 12cc74e22b35d08d2d342a1b445d967e3e09c1f6 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 22 Jun 2019 22:28:04 +0200 Subject: [PATCH 17/29] [bridge/github] correct label & comment edit mutations bugs --- bridge/github/export.go | 60 +++++++++++++++++++---------------- bridge/github/export_query.go | 38 ++++++++++++++-------- bridge/github/import_query.go | 2 +- 3 files changed, 58 insertions(+), 42 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index df8400daf1438b3d6dc330c7d4ce162a1b48c005..393146d6ba9e79a1c8effde9274363fdbc154b5e 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -426,17 +426,25 @@ func markOperationAsExported(b *cache.BugCache, target git.Hash, githubID, githu // get label from github func (ge *githubExporter) getGithubLabelID(gc *githubv4.Client, label string) (string, error) { - q := labelQuery{} - variables := map[string]interface{}{"name": label} + q := &labelQuery{} + variables := map[string]interface{}{ + "label": githubv4.String(label), + "owner": githubv4.String(ge.conf[keyOwner]), + "name": githubv4.String(ge.conf[keyProject]), + } parentCtx := context.Background() ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) defer cancel() - if err := gc.Query(ctx, &q, variables); err != nil { + if err := gc.Query(ctx, q, variables); err != nil { return "", err } + if q.Repository.Label.ID == "" { + return "", fmt.Errorf("label not found") + } + return q.Repository.Label.ID, nil } @@ -475,8 +483,6 @@ func (ge *githubExporter) createGithubLabel(gc *githubv4.Client, label, labelCol } if resp.StatusCode != http.StatusCreated { - //d, _ := ioutil.ReadAll(resp.Body) - //fmt.Println(string(d)) return "", fmt.Errorf("error creating label: response status %v", resp.StatusCode) } @@ -494,13 +500,12 @@ func (ge *githubExporter) createGithubLabel(gc *githubv4.Client, label, labelCol return "", err } - fmt.Println("ezzz", aux.NodeID) return aux.NodeID, nil } // create github label using api v4 func (ge *githubExporter) createGithubLabelV4(gc *githubv4.Client, label, labelColor string) (string, error) { - m := &createLabelMutation{} + m := createLabelMutation{} input := createLabelInput{ RepositoryID: ge.repositoryID, Name: githubv4.String(label), @@ -511,7 +516,7 @@ func (ge *githubExporter) createGithubLabelV4(gc *githubv4.Client, label, labelC ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) defer cancel() - if err := gc.Mutate(ctx, m, input, nil); err != nil { + if err := gc.Mutate(ctx, &m, input, nil); err != nil { return "", err } @@ -529,8 +534,6 @@ func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, reposito rgba := label.RGBA() hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) - fmt.Println("creating color", label, hexColor) - // create label and return id labelID, err = ge.createGithubLabel(gc, string(label), hexColor) if err != nil { @@ -620,8 +623,7 @@ func editCommentGithubIssue(gc *githubv4.Client, commentID, body string) (string return "", "", err } - comment := m.IssueComment - return commentID, comment.URL, nil + return commentID, m.UpdateIssueComment.IssueComment.URL, nil } func updateGithubIssueStatus(gc *githubv4.Client, id string, status bug.Status) error { @@ -687,7 +689,6 @@ func updateGithubIssueTitle(gc *githubv4.Client, id, title string) error { // update github issue labels func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelableID string, added, removed []bug.Label) error { - addedIDs, err := ge.getLabelsIDs(gc, labelableID, added) if err != nil { return errors.Wrap(err, "getting added labels ids") @@ -707,25 +708,28 @@ func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelable cancel() return err } - cancel() - removedIDs, err := ge.getLabelsIDs(gc, labelableID, added) - if err != nil { - return errors.Wrap(err, "getting added labels ids") - } - m2 := &removeLabelsFromLabelableMutation{} - inputRemove := githubv4.RemoveLabelsFromLabelableInput{ - LabelableID: labelableID, - LabelIDs: removedIDs, - } + if len(removed) > 0 { + removedIDs, err := ge.getLabelsIDs(gc, labelableID, removed) + if err != nil { + return errors.Wrap(err, "getting added labels ids") + } + + m2 := &removeLabelsFromLabelableMutation{} + inputRemove := githubv4.RemoveLabelsFromLabelableInput{ + LabelableID: labelableID, + LabelIDs: removedIDs, + } - ctx2, cancel2 := context.WithTimeout(parentCtx, defaultTimeout) - defer cancel2() + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + // remove label labels + if err := gc.Mutate(ctx, m2, inputRemove, nil); err != nil { + return err + } - // remove label labels - if err := gc.Mutate(ctx2, m2, inputRemove, nil); err != nil { - return err } return nil diff --git a/bridge/github/export_query.go b/bridge/github/export_query.go index 006d251126588cc7996b0996de47e8e511834499..95d4e6a9428b3d296137a0ca1299d98ad5f5fb2e 100644 --- a/bridge/github/export_query.go +++ b/bridge/github/export_query.go @@ -32,31 +32,43 @@ type addCommentToIssueMutation struct { } type updateIssueCommentMutation struct { - IssueComment struct { - ID string `graphql:"id"` - URL string `graphql:"url"` + UpdateIssueComment struct { + IssueComment struct { + ID string `graphql:"id"` + URL string `graphql:"url"` + } `graphql:"issueComment"` } `graphql:"updateIssueComment(input:$input)"` } +type removeLabelsFromLabelableMutation struct { + AddLabels struct { + Labelable struct { + Typename string `graphql:"__typename"` + } + } `graphql:"removeLabelsFromLabelable(input:$input)"` +} + +type addLabelsToLabelableMutation struct { + RemoveLabels struct { + Labelable struct { + Typename string `graphql:"__typename"` + } + } `graphql:"addLabelsToLabelable(input:$input)"` +} + type createLabelMutation struct { CreateLabel struct { Label struct { ID string `graphql:"id"` } `graphql:"label"` - } `graphql:"createLabel(input:{repositoryId: $repositoryId, name: $name, color: $color})"` -} - -type removeLabelsFromLabelableMutation struct { - AddLabels struct{} `graphql:"removeLabelsFromLabelable(input:$input)"` -} - -type addLabelsToLabelableMutation struct { - RemoveLabels struct{} `graphql:"addLabelsToLabelable(input:$input)"` + } `graphql:"createLabel(input: $input)"` } type createLabelInput struct { Color githubv4.String `json:"color"` - Description *githubv4.String `json:"description"` + Description *githubv4.String `json:"description,omitempty"` Name githubv4.String `json:"name"` RepositoryID githubv4.ID `json:"repositoryId"` + + ClientMutationID *githubv4.String `json:"clientMutationId,omitempty"` } diff --git a/bridge/github/import_query.go b/bridge/github/import_query.go index 6c0db5e14da14e1e0525980ae13da393de5db52e..34931c7d53bc98fc9206937a91e6ff8ff4f5bca4 100644 --- a/bridge/github/import_query.go +++ b/bridge/github/import_query.go @@ -173,6 +173,6 @@ type labelQuery struct { Repository struct { Label struct { ID string `graphql:"id"` - } `graphql:"label(name: $name)"` + } `graphql:"label(name: $label)"` } `graphql:"repository(owner: $owner, name: $name)"` } From fc09f2a492348f3b6013308697cbc86e15e8ab47 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sun, 23 Jun 2019 00:52:30 +0200 Subject: [PATCH 18/29] [bridge/github] exporter: Add test cases correct delete repo improve tests set user identity --- bridge/github/export.go | 5 +- bridge/github/export_test.go | 186 ++++++++++++++++++++++++++++++----- 2 files changed, 168 insertions(+), 23 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index 393146d6ba9e79a1c8effde9274363fdbc154b5e..947d5acacf66daa73bcda86301bf10a388a47256 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -35,7 +35,7 @@ type githubExporter struct { // cache identities clients identityClient map[string]*githubv4.Client - // map identity with their tokens + // map identities with their tokens identityToken map[string]string // github repository ID @@ -201,6 +201,9 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { return errors.Wrap(err, "exporting github issue") } + // incr exported bugs + ge.exportedBugs++ + hash, err := createOp.Hash() if err != nil { return errors.Wrap(err, "comment hash") diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index c3a835b2bbe84ce937ab3c93356e447b756d76fa..b60b86d8d93312249bf0a1529c9656390b508447 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -1,26 +1,76 @@ package github import ( + "bytes" + "encoding/json" "fmt" + "math/rand" + "net/http" "os" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/MichaelMure/git-bug/bridge/core" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/interrupt" - "github.com/stretchr/testify/require" ) +const ( + testRepoBaseName = "git-bug-test-github-exporter" +) + +// testCases creates bugs in repo cache +func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) (map[string]*cache.BugCache, error) { + bugs := make(map[string]*cache.BugCache) + + // simple bug + simpleBug, err := repo.NewBugRaw(identity, time.Now().Unix(), "simple bug", "new bug", nil, nil) + if err != nil { + return nil, err + } + bugs["simple bug"] = simpleBug + + /* + // bug with comments + bugWithComments, err := repo.NewBugRaw(author, time.Now().Unix(), "bug with comments", "new bug", nil, nil) + if err != nil { + return nil, err + } + + _, err = bugWithComments.AddCommentRaw(author, time.Now().Unix(), "new comment", nil, nil) + if err != nil { + return nil, err + } + bugs["bug with comments"] = bugWithComments + + // bug with label changes + bugLabelChange, err := repo.NewBugRaw(author, time.Now().Unix(), "bug label change", "new bug", nil, nil) + if err != nil { + return nil, err + } + + _, _, err = bugLabelChange.ChangeLabelsRaw(author, time.Now().Unix(), []string{"bug", "core"}, nil, nil) + if err != nil { + return nil, err + } + + _, _, err = bugLabelChange.ChangeLabelsRaw(author, time.Now().Unix(), nil, []string{"bug"}, nil) + if err != nil { + return nil, err + } + bugs["bug change label"] = bugWithComments + */ + return nil, err +} + func TestExporter(t *testing.T) { - //TODO test strategy - tests := []struct { - name string - }{ - { - name: "bug creation", - }, + user := os.Getenv("TEST_USER") + token := os.Getenv("BOT_TOKEN") + if token == "" { + t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") } repo := repository.CreateTestRepo(false) @@ -29,18 +79,44 @@ func TestExporter(t *testing.T) { backend, err := cache.NewRepoCache(repo) require.NoError(t, err) + author, err := backend.NewIdentity("test identity", "hello@testidentity.org") + if err != nil { + t.Fatal(err) + } + + err = backend.SetUserIdentity(author) + if err != nil { + t.Fatal(err) + } + defer backend.Close() interrupt.RegisterCleaner(backend.Close) - token := os.Getenv("GITHUB_TOKEN_PRIVATE") - if token == "" { - t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") + tests, err := testCases(backend, author) + if err != nil { + t.Fatal(err) } + // generate project name + projectName := generateRepoName() + fmt.Println("creating repo", projectName) + if err := createRepository(projectName, token); err != nil { + t.Fatal(err) + } + + // + // + defer func(t *testing.T) { + fmt.Println("deleting repo", projectName) + if err := deleteRepository(projectName, user, token); err != nil { + t.Fatal(err) + } + }(t) + exporter := &githubExporter{} err = exporter.Init(core.Configuration{ - keyOwner: "MichaelMure", - keyProject: "git-bug-exporter-tests", + keyOwner: user, + keyProject: projectName, keyToken: token, }) require.NoError(t, err) @@ -51,22 +127,88 @@ func TestExporter(t *testing.T) { require.NoError(t, err) fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds()) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + fmt.Println(tt.Snapshot()) + }) + } +} + +func generateRepoName() string { + rand.Seed(time.Now().UnixNano()) + var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, 8) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return fmt.Sprintf("%s-%s", testRepoBaseName, string(b)) +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { +func createRepository(project, token string) error { + url := fmt.Sprintf("%s/user/repos", githubV3Url) - }) + params := struct { + Name string `json:"name"` + Description string `json:"description"` + Private bool `json:"private"` + HasIssues bool `json:"has_issues"` + }{ + Name: project, + Description: "git-bug exporter temporary test repository", + Private: true, + HasIssues: true, } + + data, err := json.Marshal(params) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + if err != nil { + return err + } + + // need the token for private repositories + req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) + + client := &http.Client{ + Timeout: defaultTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + + return resp.Body.Close() } -func genRepoName() {} +func deleteRepository(project, owner, token string) error { + url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project) -func createRepository() {} + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } -func deleteRepository() {} + // need the token for private repositories + req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) -// verifyIssue is full -// comments -func verifyIssue() { + client := &http.Client{ + Timeout: defaultTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("error deleting repository") + } + return nil } From 0dea0f6a7673865b16e71f1998c3b0db33a54514 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sun, 23 Jun 2019 13:50:45 +0200 Subject: [PATCH 19/29] [bridge/github] simplify export operation hashs [bridge/github] exporter tests: add more test cases and global tests [bridge/github] rename export_query to export_mutation [bridge/github] exporter: Log number of exported issues and labels [bridge/github] Improve comments --- bridge/github/export.go | 38 +--- .../{export_query.go => export_mutation.go} | 0 bridge/github/export_test.go | 213 ++++++++++++++---- 3 files changed, 185 insertions(+), 66 deletions(-) rename bridge/github/{export_query.go => export_mutation.go} (100%) diff --git a/bridge/github/export.go b/bridge/github/export.go index 947d5acacf66daa73bcda86301bf10a388a47256..1ab6ebcaee545226f30846d3d8538385cb1e3e12 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -29,6 +29,9 @@ type githubExporter struct { // number of exported bugs exportedBugs int + // number of exported labels + exportedLabels int + // export only bugs taged with one of these origins onlyOrigins []string @@ -146,6 +149,7 @@ bugLoop: } } + fmt.Printf("Successfully exported %d issues and %d labels to Github\n", ge.exportedBugs, ge.exportedLabels) return nil } @@ -272,10 +276,8 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { return errors.Wrap(err, "adding comment") } - hash, err = opr.Hash() - if err != nil { - return errors.Wrap(err, "comment hash") - } + // cache comment id + ge.cachedIDs[hash.String()] = id case *bug.EditCommentOperation: @@ -311,22 +313,12 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { url = eurl } - hash, err = opr.Hash() - if err != nil { - return errors.Wrap(err, "comment hash") - } - case *bug.SetStatusOperation: opr := op.(*bug.SetStatusOperation) if err := updateGithubIssueStatus(client, bugGithubID, opr.Status); err != nil { return errors.Wrap(err, "editing status") } - hash, err = opr.Hash() - if err != nil { - return errors.Wrap(err, "set status operation hash") - } - id = bugGithubID url = bugGithubURL @@ -336,11 +328,6 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { return errors.Wrap(err, "editing title") } - hash, err = opr.Hash() - if err != nil { - return errors.Wrap(err, "set title operation hash") - } - id = bugGithubID url = bugGithubURL @@ -350,11 +337,6 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { return errors.Wrap(err, "updating labels") } - hash, err = opr.Hash() - if err != nil { - return errors.Wrap(err, "label change operation hash") - } - id = bugGithubID url = bugGithubURL @@ -367,6 +349,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { return errors.Wrap(err, "marking operation as exported") } + // commit at each operation export to avoid exporting same events multiple times if err := b.CommitAsNeeded(); err != nil { return errors.Wrap(err, "bug commit") } @@ -451,7 +434,7 @@ func (ge *githubExporter) getGithubLabelID(gc *githubv4.Client, label string) (s return q.Repository.Label.ID, nil } -func (ge *githubExporter) createGithubLabel(gc *githubv4.Client, label, labelColor string) (string, error) { +func (ge *githubExporter) createGithubLabel(label, labelColor string) (string, error) { url := fmt.Sprintf("%s/repos/%s/%s/labels", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject]) client := &http.Client{ @@ -538,11 +521,14 @@ func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, reposito hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) // create label and return id - labelID, err = ge.createGithubLabel(gc, string(label), hexColor) + // NOTE: since createLabel mutation is still in preview mode we use github api v4 to create labels + // see https://developer.github.com/v4/mutation/createlabel/ and https://developer.github.com/v4/previews/#labels-preview + labelID, err = ge.createGithubLabel(string(label), hexColor) if err != nil { return "", err } + ge.exportedLabels++ return labelID, nil } diff --git a/bridge/github/export_query.go b/bridge/github/export_mutation.go similarity index 100% rename from bridge/github/export_query.go rename to bridge/github/export_mutation.go diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index b60b86d8d93312249bf0a1529c9656390b508447..0a0c6d95c64cf0f2d9c52760dd806b9611af7e26 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -22,48 +22,142 @@ const ( testRepoBaseName = "git-bug-test-github-exporter" ) -// testCases creates bugs in repo cache -func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) (map[string]*cache.BugCache, error) { - bugs := make(map[string]*cache.BugCache) +type testCase struct { + name string + bug *cache.BugCache + numOrOp int // number of original operations +} +func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCase, error) { // simple bug - simpleBug, err := repo.NewBugRaw(identity, time.Now().Unix(), "simple bug", "new bug", nil, nil) + simpleBug, err := repo.NewBug("simple bug", "new bug") if err != nil { return nil, err } - bugs["simple bug"] = simpleBug - /* - // bug with comments - bugWithComments, err := repo.NewBugRaw(author, time.Now().Unix(), "bug with comments", "new bug", nil, nil) - if err != nil { - return nil, err - } + // bug with comments + bugWithComments, err := repo.NewBug("bug with comments", "new bug") + if err != nil { + return nil, err + } - _, err = bugWithComments.AddCommentRaw(author, time.Now().Unix(), "new comment", nil, nil) - if err != nil { - return nil, err - } - bugs["bug with comments"] = bugWithComments + _, err = bugWithComments.AddComment("new comment") + if err != nil { + return nil, err + } - // bug with label changes - bugLabelChange, err := repo.NewBugRaw(author, time.Now().Unix(), "bug label change", "new bug", nil, nil) - if err != nil { - return nil, err - } + // bug with label changes + bugLabelChange, err := repo.NewBug("bug label change", "new bug") + if err != nil { + return nil, err + } - _, _, err = bugLabelChange.ChangeLabelsRaw(author, time.Now().Unix(), []string{"bug", "core"}, nil, nil) - if err != nil { - return nil, err - } + _, _, err = bugLabelChange.ChangeLabels([]string{"bug"}, nil) + if err != nil { + return nil, err + } + + _, _, err = bugLabelChange.ChangeLabels([]string{"core"}, nil) + if err != nil { + return nil, err + } + + _, _, err = bugLabelChange.ChangeLabels(nil, []string{"bug"}) + if err != nil { + return nil, err + } + + // bug with comments editions + bugWithCommentEditions, err := repo.NewBug("bug with comments editions", "new bug") + if err != nil { + return nil, err + } + + createOpHash, err := bugWithCommentEditions.Snapshot().Operations[0].Hash() + if err != nil { + return nil, err + } + + _, err = bugWithCommentEditions.EditComment(createOpHash, "first comment edited") + if err != nil { + return nil, err + } + + commentOp, err := bugWithCommentEditions.AddComment("first comment") + if err != nil { + return nil, err + } + + commentOpHash, err := commentOp.Hash() + if err != nil { + return nil, err + } + + _, err = bugWithCommentEditions.EditComment(commentOpHash, "first comment edited") + if err != nil { + return nil, err + } + + // bug status changed + bugStatusChanged, err := repo.NewBug("bug status changed", "new bug") + if err != nil { + return nil, err + } + + _, err = bugStatusChanged.Close() + if err != nil { + return nil, err + } + + _, err = bugStatusChanged.Open() + if err != nil { + return nil, err + } + + // bug title changed + bugTitleEdited, err := repo.NewBug("bug title edited", "new bug") + if err != nil { + return nil, err + } + + _, err = bugTitleEdited.SetTitle("bug title edited again") + if err != nil { + return nil, err + } + + return []*testCase{ + &testCase{ + name: "simple bug", + bug: simpleBug, + numOrOp: 1, + }, + &testCase{ + name: "bug with comments", + bug: bugWithComments, + numOrOp: 2, + }, + &testCase{ + name: "bug label change", + bug: bugLabelChange, + numOrOp: 4, + }, + &testCase{ + name: "bug with comment editions", + bug: bugWithCommentEditions, + numOrOp: 4, + }, + &testCase{ + name: "bug changed status", + bug: bugStatusChanged, + numOrOp: 3, + }, + &testCase{ + name: "bug title edited", + bug: bugTitleEdited, + numOrOp: 2, + }, + }, nil - _, _, err = bugLabelChange.ChangeLabelsRaw(author, time.Now().Unix(), nil, []string{"bug"}, nil) - if err != nil { - return nil, err - } - bugs["bug change label"] = bugWithComments - */ - return nil, err } func TestExporter(t *testing.T) { @@ -79,7 +173,7 @@ func TestExporter(t *testing.T) { backend, err := cache.NewRepoCache(repo) require.NoError(t, err) - author, err := backend.NewIdentity("test identity", "hello@testidentity.org") + author, err := backend.NewIdentity("test identity", "test@test.org") if err != nil { t.Fatal(err) } @@ -99,20 +193,22 @@ func TestExporter(t *testing.T) { // generate project name projectName := generateRepoName() - fmt.Println("creating repo", projectName) + + // create repository if err := createRepository(projectName, token); err != nil { t.Fatal(err) } + fmt.Println("created repository", projectName) - // - // + // delete repository before ending tests defer func(t *testing.T) { - fmt.Println("deleting repo", projectName) if err := deleteRepository(projectName, user, token); err != nil { t.Fatal(err) } + fmt.Println("deleted repository:", projectName) }(t) + // initialize exporter exporter := &githubExporter{} err = exporter.Init(core.Configuration{ keyOwner: user, @@ -123,13 +219,50 @@ func TestExporter(t *testing.T) { start := time.Now() + // export all bugs err = exporter.ExportAll(backend, time.Time{}) require.NoError(t, err) fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds()) - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - fmt.Println(tt.Snapshot()) + + repoTwo := repository.CreateTestRepo(false) + defer repository.CleanupTestRepos(t, repoTwo) + + // create a second backend + backendTwo, err := cache.NewRepoCache(repoTwo) + require.NoError(t, err) + + importer := &githubImporter{} + err = importer.Init(core.Configuration{ + keyOwner: user, + keyProject: projectName, + keyToken: token, + }) + require.NoError(t, err) + + // import all exported bugs to the second backend + err = importer.ImportAll(backendTwo, time.Time{}) + require.NoError(t, err) + + require.Len(t, backendTwo.AllBugsIds(), len(tests)) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // for each operation a SetMetadataOperation will be added + // so number of operations should double + require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2) + + bugGithubID, ok := tt.bug.Snapshot().Operations[0].GetMetadata(keyGithubId) + require.True(t, ok) + + importedBug, err := backendTwo.ResolveBugCreateMetadata(keyGithubId, bugGithubID) + require.NoError(t, err) + + require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp) + + for _, _ = range importedBug.Snapshot().Operations { + // test operations or last bug state ? + } }) } } From 9b4b1fc09a9fca1aeb48560a258de8990070b3b5 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sun, 23 Jun 2019 14:50:15 +0200 Subject: [PATCH 20/29] Rebase and add bridge push zsh & powershell completion --- misc/powershell_completion/git-bug | 4 +++ misc/zsh_completion/git-bug | 57 +++++------------------------- 2 files changed, 12 insertions(+), 49 deletions(-) diff --git a/misc/powershell_completion/git-bug b/misc/powershell_completion/git-bug index 7eff1cda5d970643507bfd816d59b195e843a87a..b89897a305205240887e1bcca6102c8a7686b4e7 100644 --- a/misc/powershell_completion/git-bug +++ b/misc/powershell_completion/git-bug @@ -50,6 +50,7 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { 'git-bug;bridge' { [CompletionResult]::new('configure', 'configure', [CompletionResultType]::ParameterValue, 'Configure a new bridge.') [CompletionResult]::new('pull', 'pull', [CompletionResultType]::ParameterValue, 'Pull updates.') + [CompletionResult]::new('push', 'push', [CompletionResultType]::ParameterValue, 'Push updates.') [CompletionResult]::new('rm', 'rm', [CompletionResultType]::ParameterValue, 'Delete a configured bridge.') break } @@ -71,6 +72,9 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock { 'git-bug;bridge;pull' { break } + 'git-bug;bridge;push' { + break + } 'git-bug;bridge;rm' { break } diff --git a/misc/zsh_completion/git-bug b/misc/zsh_completion/git-bug index 97fb27c58831586717620e7229a1bc90f9bccaf3..1e2ed5116b2ee764ed5a33ae7e8cb3e42bfd5a04 100644 --- a/misc/zsh_completion/git-bug +++ b/misc/zsh_completion/git-bug @@ -1,4 +1,3 @@ -<<<<<<< HEAD #compdef _git-bug git-bug @@ -117,6 +116,7 @@ function _git-bug_bridge { commands=( "configure:Configure a new bridge." "pull:Pull updates." + "push:Push updates." "rm:Delete a configured bridge." ) _describe "command" commands @@ -130,6 +130,9 @@ function _git-bug_bridge { pull) _git-bug_bridge_pull ;; + push) + _git-bug_bridge_push + ;; rm) _git-bug_bridge_rm ;; @@ -150,6 +153,10 @@ function _git-bug_bridge_pull { _arguments } +function _git-bug_bridge_push { + _arguments +} + function _git-bug_bridge_rm { _arguments } @@ -395,51 +402,3 @@ function _git-bug_webui { '(-p --port)'{-p,--port}'[Port to listen to (default is random)]:' } -======= -#compdef git-bug - -_arguments \ - '1: :->level1' \ - '2: :->level2' \ - '3: :_files' -case $state in - level1) - case $words[1] in - git-bug) - _arguments '1: :(add bridge commands comment deselect label ls ls-id ls-label pull push select show status termui title user version webui)' - ;; - *) - _arguments '*: :_files' - ;; - esac - ;; - level2) - case $words[2] in - bridge) - _arguments '2: :(configure pull push rm)' - ;; - comment) - _arguments '2: :(add)' - ;; - label) - _arguments '2: :(add rm)' - ;; - status) - _arguments '2: :(close open)' - ;; - title) - _arguments '2: :(edit)' - ;; - user) - _arguments '2: :(adopt create ls)' - ;; - *) - _arguments '*: :_files' - ;; - esac - ;; - *) - _arguments '*: :_files' - ;; -esac ->>>>>>> 3daac46... update man docs and completion scripts From 87cdf03219e7713f041387cbfc3144506a6be9da Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sun, 23 Jun 2019 15:18:56 +0200 Subject: [PATCH 21/29] [bridge/github] exporter tests: Test issues metadata [bridge/github] exproter tests: Change env vars --- bridge/github/export_test.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 0a0c6d95c64cf0f2d9c52760dd806b9611af7e26..85f5eb07de98bb4fac722b672b27b002e99d76e2 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/bug" "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/interrupt" @@ -162,9 +163,9 @@ func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCas func TestExporter(t *testing.T) { user := os.Getenv("TEST_USER") - token := os.Getenv("BOT_TOKEN") + token := os.Getenv("GITHUB_TOKEN_ADMIN") if token == "" { - t.Skip("Env var GITHUB_TOKEN_PRIVATE missing") + t.Skip("Env var GITHUB_TOKEN_ADMIN missing") } repo := repository.CreateTestRepo(false) @@ -252,6 +253,16 @@ func TestExporter(t *testing.T) { // so number of operations should double require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2) + for _, op := range tt.bug.Snapshot().Operations { + if _, ok := op.(*bug.SetMetadataOperation); !ok { + _, haveIDMetadata := op.GetMetadata(keyGithubId) + require.True(t, haveIDMetadata) + + _, haveURLMetada := op.GetMetadata(keyGithubUrl) + require.True(t, haveURLMetada) + } + } + bugGithubID, ok := tt.bug.Snapshot().Operations[0].GetMetadata(keyGithubId) require.True(t, ok) @@ -260,6 +271,10 @@ func TestExporter(t *testing.T) { require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp) + issueOrigin, ok := importedBug.Snapshot().Operations[0].GetMetadata(keyOrigin) + require.True(t, ok) + require.Equal(t, issueOrigin, target) + for _, _ = range importedBug.Snapshot().Operations { // test operations or last bug state ? } From 31eebdf9da8cd0f6afd7999175fb53cc135a1833 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sun, 23 Jun 2019 17:54:38 +0200 Subject: [PATCH 22/29] [bridge/github] Correcte some types and add comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit General improvements Co-Authored-By: Michael Muré empty array check an empty array is not nil Co-Authored-By: Michael Muré --- bridge/github/export.go | 20 +++++++++++++------- bridge/github/export_test.go | 25 +++++++++++++++++++------ bug/operation.go | 3 ++- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index 1ab6ebcaee545226f30846d3d8538385cb1e3e12..9e3943343ea957c5ca3a945fe762f849db258430 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -22,7 +22,7 @@ var ( ErrMissingIdentityToken = errors.New("missing identity token") ) -// githubImporter implement the Importer interface +// githubExporter implement the Exporter interface type githubExporter struct { conf core.Configuration @@ -63,8 +63,11 @@ func (ge *githubExporter) Init(conf core.Configuration) error { return nil } +// allowOrigin verify that origin is allowed to get exported. +// if the exporter was initialized with no specified origins, it will return +// true for all origins func (ge *githubExporter) allowOrigin(origin string) bool { - if ge.onlyOrigins == nil { + if len(ge.onlyOrigins) == 0 { return true } @@ -78,7 +81,7 @@ func (ge *githubExporter) allowOrigin(origin string) bool { } // getIdentityClient return an identity github api v4 client -// if no client were found it will initilize it from the known tokens map and cache it for next it use +// if no client were found it will initilize it from the known tokens map and cache it for next use func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) { client, ok := ge.identityClient[id] if ok { @@ -143,6 +146,7 @@ bugLoop: return err } + // avoid calling exportBug multiple times for the same bug continue bugLoop } } @@ -163,7 +167,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // Special case: // if a user try to export a bug that is not already exported to Github (or imported - // from Github) and he is not the author of the bug. There is nothing we can do. + // from Github) and we do not have the token of the bug author, there is nothing we can do. // first operation is always createOp createOp := snapshot.Operations[0].(*bug.CreateOperation) @@ -184,6 +188,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // if we find github ID, github URL must be found too panic("expected to find github issue URL") } + // will be used to mark operation related to a bug as exported bugGithubID = githubID bugGithubURL = githubURL @@ -427,6 +432,7 @@ func (ge *githubExporter) getGithubLabelID(gc *githubv4.Client, label string) (s return "", err } + // if label id is empty, it means there is no such label in this Github repository if q.Repository.Label.ID == "" { return "", fmt.Errorf("label not found") } @@ -434,7 +440,7 @@ func (ge *githubExporter) getGithubLabelID(gc *githubv4.Client, label string) (s return q.Repository.Label.ID, nil } -func (ge *githubExporter) createGithubLabel(label, labelColor string) (string, error) { +func (ge *githubExporter) createGithubLabel(label, color string) (string, error) { url := fmt.Sprintf("%s/repos/%s/%s/labels", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject]) client := &http.Client{ @@ -447,7 +453,7 @@ func (ge *githubExporter) createGithubLabel(label, labelColor string) (string, e Description string `json:"description"` }{ Name: label, - Color: labelColor, + Color: color, } data, err := json.Marshal(params) @@ -516,7 +522,7 @@ func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, reposito return labelID, nil } - // hex color + // RGBA to hex color rgba := label.RGBA() hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 85f5eb07de98bb4fac722b672b27b002e99d76e2..8f0f786934e3241f7f5bb5fb1f51a150bd69a180 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -161,19 +161,24 @@ func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCas } -func TestExporter(t *testing.T) { +func TestPushPull(t *testing.T) { + // repo owner user := os.Getenv("TEST_USER") + + // token must have 'repo' and 'delete_repo' scopes token := os.Getenv("GITHUB_TOKEN_ADMIN") if token == "" { t.Skip("Env var GITHUB_TOKEN_ADMIN missing") } + // create repo backend repo := repository.CreateTestRepo(false) defer repository.CleanupTestRepos(t, repo) backend, err := cache.NewRepoCache(repo) require.NoError(t, err) + // set author identity author, err := backend.NewIdentity("test identity", "test@test.org") if err != nil { t.Fatal(err) @@ -195,13 +200,13 @@ func TestExporter(t *testing.T) { // generate project name projectName := generateRepoName() - // create repository + // create target Github repository if err := createRepository(projectName, token); err != nil { t.Fatal(err) } fmt.Println("created repository", projectName) - // delete repository before ending tests + // Make sure to remove the Github repository when the test end defer func(t *testing.T) { if err := deleteRepository(projectName, user, token); err != nil { t.Fatal(err) @@ -253,7 +258,9 @@ func TestExporter(t *testing.T) { // so number of operations should double require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2) + // verify operation have correcte metadata for _, op := range tt.bug.Snapshot().Operations { + // Check if the originals operations (*not* SetMetadata) are tagged properly if _, ok := op.(*bug.SetMetadataOperation); !ok { _, haveIDMetadata := op.GetMetadata(keyGithubId) require.True(t, haveIDMetadata) @@ -263,21 +270,23 @@ func TestExporter(t *testing.T) { } } + // get bug github ID bugGithubID, ok := tt.bug.Snapshot().Operations[0].GetMetadata(keyGithubId) require.True(t, ok) + // retrive bug from backendTwo importedBug, err := backendTwo.ResolveBugCreateMetadata(keyGithubId, bugGithubID) require.NoError(t, err) + // verify bug have same number of original operations require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp) + // verify bugs are taged with origin=github issueOrigin, ok := importedBug.Snapshot().Operations[0].GetMetadata(keyOrigin) require.True(t, ok) require.Equal(t, issueOrigin, target) - for _, _ = range importedBug.Snapshot().Operations { - // test operations or last bug state ? - } + //TODO: maybe more tests to ensure bug final state }) } } @@ -292,7 +301,9 @@ func generateRepoName() string { return fmt.Sprintf("%s-%s", testRepoBaseName, string(b)) } +// create repository need a token with scope 'repo' func createRepository(project, token string) error { +// This function use the V3 Github API because repository creation is not supported yet on the V4 API. url := fmt.Sprintf("%s/user/repos", githubV3Url) params := struct { @@ -332,7 +343,9 @@ func createRepository(project, token string) error { return resp.Body.Close() } +// delete repository need a token with scope 'delete_repo' func deleteRepository(project, owner, token string) error { +// This function use the V3 Github API because repository removal is not supported yet on the V4 API. url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project) req, err := http.NewRequest("DELETE", url, nil) diff --git a/bug/operation.go b/bug/operation.go index 2cb27d18cba34a021ba53fa76d2c85da8b60a7d8..daef7b8c33f24d5fb5b7f94419a5a7a9aca3357b 100644 --- a/bug/operation.go +++ b/bug/operation.go @@ -49,7 +49,7 @@ type Operation interface { GetMetadata(key string) (string, bool) // AllMetadata return all metadata for this operation AllMetadata() map[string]string - // GetAuthor return author identity + // GetAuthor return the author identity GetAuthor() identity.Interface } @@ -225,6 +225,7 @@ func (op *OpBase) AllMetadata() map[string]string { return result } +// GetAuthor return author identity func (op *OpBase) GetAuthor() identity.Interface { return op.Author } From 570ae5f75e034523a1d7d94a04febe944d36399b Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sun, 23 Jun 2019 19:20:10 +0200 Subject: [PATCH 23/29] [bug] add snapshot.GetCreateMetadata method [bug] add snapshot.HasParticipant(id string) [bug] add snapshot.HasAnyParticipant(ids ...string) [bug] add snapshot.HasActor(id string) [bug] add snapshot.HasAnyActor(ids ...string) [bridge/github] improve comments [bridge/github] exporter tests: register deleteRepository in cleaner [bridge/github] tests rebase --- bridge/github/export.go | 67 ++++++++++++++++---------------- bridge/github/export_mutation.go | 4 +- bridge/github/export_test.go | 26 +++++++------ bug/snapshot.go | 45 +++++++++++++++++++++ 4 files changed, 96 insertions(+), 46 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index 9e3943343ea957c5ca3a945fe762f849db258430..00ba158527b4ad40f68241b3d4b1a7261debd7eb 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -122,8 +122,13 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) erro return err } + allIdentitiesIds := []string{} + for id := range ge.identityToken { + allIdentitiesIds = append(allIdentitiesIds, id) + } + allBugsIds := repo.AllBugsIds() -bugLoop: + for _, id := range allBugsIds { b, err := repo.ResolveBug(id) if err != nil { @@ -137,20 +142,13 @@ bugLoop: continue } - for _, p := range snapshot.Participants { - // if we have a token for one of the participants - for userId := range ge.identityToken { - if p.Id() == userId { - // try to export the bug and it associated events - if err := ge.exportBug(b, since); err != nil { - return err - } - - // avoid calling exportBug multiple times for the same bug - continue bugLoop - } + if snapshot.HasAnyParticipant(allIdentitiesIds...) { + // try to export the bug and it associated events + if err := ge.exportBug(b, since); err != nil { + return err } } + } fmt.Printf("Successfully exported %d issues and %d labels to Github\n", ge.exportedBugs, ge.exportedLabels) @@ -171,7 +169,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // first operation is always createOp createOp := snapshot.Operations[0].(*bug.CreateOperation) - author := createOp.GetAuthor() + author := snapshot.Author // skip bug if origin is not allowed origin, ok := createOp.GetMetadata(keyOrigin) @@ -440,6 +438,9 @@ func (ge *githubExporter) getGithubLabelID(gc *githubv4.Client, label string) (s return q.Repository.Label.ID, nil } +// create a new label and return it github id +// NOTE: since createLabel mutation is still in preview mode we use github api v3 to create labels +// see https://developer.github.com/v4/mutation/createlabel/ and https://developer.github.com/v4/previews/#labels-preview func (ge *githubExporter) createGithubLabel(label, color string) (string, error) { url := fmt.Sprintf("%s/repos/%s/%s/labels", githubV3Url, ge.conf[keyOwner], ge.conf[keyProject]) @@ -495,6 +496,7 @@ func (ge *githubExporter) createGithubLabel(label, color string) (string, error) return aux.NodeID, nil } +/** // create github label using api v4 func (ge *githubExporter) createGithubLabelV4(gc *githubv4.Client, label, labelColor string) (string, error) { m := createLabelMutation{} @@ -514,6 +516,7 @@ func (ge *githubExporter) createGithubLabelV4(gc *githubv4.Client, label, labelC return m.CreateLabel.Label.ID, nil } +*/ func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, repositoryID string, label bug.Label) (string, error) { // try to get label id @@ -526,9 +529,6 @@ func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, reposito rgba := label.RGBA() hexColor := fmt.Sprintf("%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B) - // create label and return id - // NOTE: since createLabel mutation is still in preview mode we use github api v4 to create labels - // see https://developer.github.com/v4/mutation/createlabel/ and https://developer.github.com/v4/previews/#labels-preview labelID, err = ge.createGithubLabel(string(label), hexColor) if err != nil { return "", err @@ -705,26 +705,27 @@ func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelable } cancel() - if len(removed) > 0 { - removedIDs, err := ge.getLabelsIDs(gc, labelableID, removed) - if err != nil { - return errors.Wrap(err, "getting added labels ids") - } + if len(removed) == 0 { + return nil + } - m2 := &removeLabelsFromLabelableMutation{} - inputRemove := githubv4.RemoveLabelsFromLabelableInput{ - LabelableID: labelableID, - LabelIDs: removedIDs, - } + removedIDs, err := ge.getLabelsIDs(gc, labelableID, removed) + if err != nil { + return errors.Wrap(err, "getting added labels ids") + } - ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) - defer cancel() + m2 := &removeLabelsFromLabelableMutation{} + inputRemove := githubv4.RemoveLabelsFromLabelableInput{ + LabelableID: labelableID, + LabelIDs: removedIDs, + } - // remove label labels - if err := gc.Mutate(ctx, m2, inputRemove, nil); err != nil { - return err - } + ctx, cancel = context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + // remove label labels + if err := gc.Mutate(ctx, m2, inputRemove, nil); err != nil { + return err } return nil diff --git a/bridge/github/export_mutation.go b/bridge/github/export_mutation.go index 95d4e6a9428b3d296137a0ca1299d98ad5f5fb2e..cf77f344f855a4c7eba50e90d4736bdf5528726f 100644 --- a/bridge/github/export_mutation.go +++ b/bridge/github/export_mutation.go @@ -1,7 +1,5 @@ package github -import "github.com/shurcooL/githubv4" - type createIssueMutation struct { CreateIssue struct { Issue struct { @@ -56,6 +54,7 @@ type addLabelsToLabelableMutation struct { } `graphql:"addLabelsToLabelable(input:$input)"` } +/** type createLabelMutation struct { CreateLabel struct { Label struct { @@ -72,3 +71,4 @@ type createLabelInput struct { ClientMutationID *githubv4.String `json:"clientMutationId,omitempty"` } +*/ diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 8f0f786934e3241f7f5bb5fb1f51a150bd69a180..ff8b70b8a053782285be0ae55ddf9d463ac6cfda 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -31,13 +31,13 @@ type testCase struct { func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCase, error) { // simple bug - simpleBug, err := repo.NewBug("simple bug", "new bug") + simpleBug, _, err := repo.NewBug("simple bug", "new bug") if err != nil { return nil, err } // bug with comments - bugWithComments, err := repo.NewBug("bug with comments", "new bug") + bugWithComments, _, err := repo.NewBug("bug with comments", "new bug") if err != nil { return nil, err } @@ -48,7 +48,7 @@ func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCas } // bug with label changes - bugLabelChange, err := repo.NewBug("bug label change", "new bug") + bugLabelChange, _, err := repo.NewBug("bug label change", "new bug") if err != nil { return nil, err } @@ -69,12 +69,12 @@ func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCas } // bug with comments editions - bugWithCommentEditions, err := repo.NewBug("bug with comments editions", "new bug") + bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug") if err != nil { return nil, err } - createOpHash, err := bugWithCommentEditions.Snapshot().Operations[0].Hash() + createOpHash, err := createOp.Hash() if err != nil { return nil, err } @@ -100,7 +100,7 @@ func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCas } // bug status changed - bugStatusChanged, err := repo.NewBug("bug status changed", "new bug") + bugStatusChanged, _, err := repo.NewBug("bug status changed", "new bug") if err != nil { return nil, err } @@ -116,7 +116,7 @@ func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCas } // bug title changed - bugTitleEdited, err := repo.NewBug("bug title edited", "new bug") + bugTitleEdited, _, err := repo.NewBug("bug title edited", "new bug") if err != nil { return nil, err } @@ -214,6 +214,10 @@ func TestPushPull(t *testing.T) { fmt.Println("deleted repository:", projectName) }(t) + interrupt.RegisterCleaner(func() error { + return deleteRepository(projectName, user, token) + }) + // initialize exporter exporter := &githubExporter{} err = exporter.Init(core.Configuration{ @@ -271,7 +275,7 @@ func TestPushPull(t *testing.T) { } // get bug github ID - bugGithubID, ok := tt.bug.Snapshot().Operations[0].GetMetadata(keyGithubId) + bugGithubID, ok := tt.bug.Snapshot().GetCreateMetadata(keyGithubId) require.True(t, ok) // retrive bug from backendTwo @@ -282,7 +286,7 @@ func TestPushPull(t *testing.T) { require.Len(t, importedBug.Snapshot().Operations, tt.numOrOp) // verify bugs are taged with origin=github - issueOrigin, ok := importedBug.Snapshot().Operations[0].GetMetadata(keyOrigin) + issueOrigin, ok := importedBug.Snapshot().GetCreateMetadata(keyOrigin) require.True(t, ok) require.Equal(t, issueOrigin, target) @@ -303,7 +307,7 @@ func generateRepoName() string { // create repository need a token with scope 'repo' func createRepository(project, token string) error { -// This function use the V3 Github API because repository creation is not supported yet on the V4 API. + // This function use the V3 Github API because repository creation is not supported yet on the V4 API. url := fmt.Sprintf("%s/user/repos", githubV3Url) params := struct { @@ -345,7 +349,7 @@ func createRepository(project, token string) error { // delete repository need a token with scope 'delete_repo' func deleteRepository(project, owner, token string) error { -// This function use the V3 Github API because repository removal is not supported yet on the V4 API. + // This function use the V3 Github API because repository removal is not supported yet on the V4 API. url := fmt.Sprintf("%s/repos/%s/%s", githubV3Url, owner, project) req, err := http.NewRequest("DELETE", url, nil) diff --git a/bug/snapshot.go b/bug/snapshot.go index a4661b14928c90308ad71dc2a8dfa16adf8d09f2..114b1123f62143d540645dc5715fb4465ba200af 100644 --- a/bug/snapshot.go +++ b/bug/snapshot.go @@ -54,6 +54,11 @@ func (snap *Snapshot) LastEditUnix() int64 { return snap.Operations[len(snap.Operations)-1].GetUnixTime() } +// GetCreateMetadata return the creation metadata +func (snap *Snapshot) GetCreateMetadata(key string) (string, bool) { + return snap.Operations[0].GetMetadata(key) +} + // SearchTimelineItem will search in the timeline for an item matching the given hash func (snap *Snapshot) SearchTimelineItem(hash git.Hash) (TimelineItem, error) { for i := range snap.Timeline { @@ -87,5 +92,45 @@ func (snap *Snapshot) addParticipant(participant identity.Interface) { snap.Participants = append(snap.Participants, participant) } +// HasParticipant return true if the id is a participant +func (snap *Snapshot) HasParticipant(id string) bool { + for _, p := range snap.Participants { + if p.Id() == id { + return true + } + } + return false +} + +// HasAnyParticipant return true if one of the ids is a participant +func (snap *Snapshot) HasAnyParticipant(ids ...string) bool { + for _, id := range ids { + if snap.HasParticipant(id) { + return true + } + } + return false +} + +// HasActor return true if the id is a actor +func (snap *Snapshot) HasActor(id string) bool { + for _, p := range snap.Actors { + if p.Id() == id { + return true + } + } + return false +} + +// HasAnyActor return true if one of the ids is a actor +func (snap *Snapshot) HasAnyActor(ids ...string) bool { + for _, id := range ids { + if snap.HasActor(id) { + return true + } + } + return false +} + // Sign post method for gqlgen func (snap *Snapshot) IsAuthored() {} From 1f365b236900d429cbfdaa1807163a110e6e4624 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Tue, 25 Jun 2019 01:33:48 +0200 Subject: [PATCH 24/29] [core] Implement ExportResults Use ExportResult chan to send export events Remove exportedBugs and exportedLabels --- bridge/core/bridge.go | 10 +- bridge/core/export.go | 101 +++++++++++++++++++ bridge/core/interfaces.go | 2 +- bridge/github/export.go | 185 +++++++++++++++++++++-------------- bridge/github/export_test.go | 24 ++--- commands/bridge_push.go | 11 ++- 6 files changed, 241 insertions(+), 92 deletions(-) create mode 100644 bridge/core/export.go diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go index 1b960e0e1fea5f4d94d82999ce9b7bf109155e65..30e051be338a9cb24651cce76c261a9f845270b9 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -297,21 +297,21 @@ func (b *Bridge) ImportAll(since time.Time) error { return importer.ImportAll(b.repo, since) } -func (b *Bridge) ExportAll(since time.Time) error { +func (b *Bridge) ExportAll(since time.Time) (<-chan ExportResult, error) { exporter := b.getExporter() if exporter == nil { - return ErrExportNotSupported + return nil, ErrExportNotSupported } err := b.ensureConfig() if err != nil { - return err + return nil, err } err = b.ensureInit() if err != nil { - return err + return nil, err } - return exporter.ExportAll(b.repo, since) + return exporter.ExportAll(b.repo, since), nil } diff --git a/bridge/core/export.go b/bridge/core/export.go new file mode 100644 index 0000000000000000000000000000000000000000..5114943025b9b674e756988348b3956af386d083 --- /dev/null +++ b/bridge/core/export.go @@ -0,0 +1,101 @@ +package core + +import "fmt" + +type EventStatus int + +const ( + _ EventStatus = iota + EventStatusBug + EventStatusComment + EventStatusCommentEdition + EventStatusStatusChange + EventStatusTitleEdition + EventStatusLabelChange + EventStatusNothing +) + +type ExportResult struct { + Err error + Event EventStatus + ID string + Reason string +} + +func (er ExportResult) String() string { + switch er.Event { + case EventStatusBug: + return "new issue" + case EventStatusComment: + return "new comment" + case EventStatusCommentEdition: + return "updated comment" + case EventStatusStatusChange: + return "changed status" + case EventStatusTitleEdition: + return "changed title" + case EventStatusLabelChange: + return "changed label" + case EventStatusNothing: + return fmt.Sprintf("no event: %v", er.Reason) + default: + panic("unknown export result") + } +} + +func NewExportError(err error, reason string) ExportResult { + return ExportResult{ + Err: err, + Reason: reason, + } +} + +func NewExportNothing(id string, reason string) ExportResult { + return ExportResult{ + ID: id, + Reason: reason, + Event: EventStatusNothing, + } +} + +func NewExportBug(id string) ExportResult { + return ExportResult{ + ID: id, + Event: EventStatusBug, + } +} + +func NewExportComment(id string) ExportResult { + return ExportResult{ + ID: id, + Event: EventStatusComment, + } +} + +func NewExportCommentEdition(id string) ExportResult { + return ExportResult{ + ID: id, + Event: EventStatusCommentEdition, + } +} + +func NewExportStatusChange(id string) ExportResult { + return ExportResult{ + ID: id, + Event: EventStatusStatusChange, + } +} + +func NewExportLabelChange(id string) ExportResult { + return ExportResult{ + ID: id, + Event: EventStatusLabelChange, + } +} + +func NewExportTitleEdition(id string) ExportResult { + return ExportResult{ + ID: id, + Event: EventStatusTitleEdition, + } +} diff --git a/bridge/core/interfaces.go b/bridge/core/interfaces.go index 37fdb3d7233d71b2348960c43401f2bd0240a3c8..5cc7006a168f5b1d83097354c9f2a8ee243b9e7d 100644 --- a/bridge/core/interfaces.go +++ b/bridge/core/interfaces.go @@ -34,5 +34,5 @@ type Importer interface { type Exporter interface { Init(conf Configuration) error - ExportAll(repo *cache.RepoCache, since time.Time) error + ExportAll(repo *cache.RepoCache, since time.Time) <-chan ExportResult } diff --git a/bridge/github/export.go b/bridge/github/export.go index 00ba158527b4ad40f68241b3d4b1a7261debd7eb..40044ca11b54ab13ef1060ffc3e7ad4369d9480c 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -26,13 +26,7 @@ var ( type githubExporter struct { conf core.Configuration - // number of exported bugs - exportedBugs int - - // number of exported labels - exportedLabels int - - // export only bugs taged with one of these origins + // export only bugs tagged with one of these origins onlyOrigins []string // cache identities clients @@ -81,7 +75,7 @@ func (ge *githubExporter) allowOrigin(origin string) bool { } // getIdentityClient return an identity github api v4 client -// if no client were found it will initilize it from the known tokens map and cache it for next use +// if no client were found it will initialize it from the known tokens map and cache it for next use func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) { client, ok := ge.identityClient[id] if ok { @@ -103,60 +97,68 @@ func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) } // ExportAll export all event made by the current user to Github -func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) error { - user, err := repo.GetUserIdentity() - if err != nil { - return err - } +func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) <-chan core.ExportResult { + out := make(chan core.ExportResult) - ge.identityToken[user.Id()] = ge.conf[keyToken] + go func(out chan<- core.ExportResult) { + defer close(out) - // get repository node id - ge.repositoryID, err = getRepositoryNodeID( - ge.conf[keyOwner], - ge.conf[keyProject], - ge.conf[keyToken], - ) - - if err != nil { - return err - } + user, err := repo.GetUserIdentity() + if err != nil { + out <- core.NewExportError(err, "") + return + } - allIdentitiesIds := []string{} - for id := range ge.identityToken { - allIdentitiesIds = append(allIdentitiesIds, id) - } + ge.identityToken[user.Id()] = ge.conf[keyToken] - allBugsIds := repo.AllBugsIds() + // get repository node id + ge.repositoryID, err = getRepositoryNodeID( + ge.conf[keyOwner], + ge.conf[keyProject], + ge.conf[keyToken], + ) - for _, id := range allBugsIds { - b, err := repo.ResolveBug(id) if err != nil { - return err + out <- core.NewExportError(err, ge.repositoryID) + return } - snapshot := b.Snapshot() - - // ignore issues created before since date - if snapshot.CreatedAt.Before(since) { - continue + var allIdentitiesIds []string + for id := range ge.identityToken { + allIdentitiesIds = append(allIdentitiesIds, id) } - if snapshot.HasAnyParticipant(allIdentitiesIds...) { - // try to export the bug and it associated events - if err := ge.exportBug(b, since); err != nil { - return err + allBugsIds := repo.AllBugsIds() + + for _, id := range allBugsIds { + b, err := repo.ResolveBug(id) + if err != nil { + out <- core.NewExportError(err, id) + return } - } - } + snapshot := b.Snapshot() - fmt.Printf("Successfully exported %d issues and %d labels to Github\n", ge.exportedBugs, ge.exportedLabels) - return nil + // ignore issues created before since date + if snapshot.CreatedAt.Before(since) { + out <- core.NewExportNothing(b.Id(), "bug created before the since date") + continue + } + + if snapshot.HasAnyActor(allIdentitiesIds...) { + // try to export the bug and it associated events + ge.exportBug(b, since, out) + } else { + out <- core.NewExportNothing(id, "not an actor") + } + } + }(out) + + return out } // exportBug publish bugs and related events -func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { +func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan<- core.ExportResult) { snapshot := b.Snapshot() var bugGithubID string @@ -174,8 +176,8 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // skip bug if origin is not allowed origin, ok := createOp.GetMetadata(keyOrigin) if ok && !ge.allowOrigin(origin) { - // TODO print a warn ? - return nil + out <- core.NewExportNothing(b.Id(), fmt.Sprintf("issue taged with origin: %s", origin)) + return } // get github bug ID @@ -187,6 +189,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { panic("expected to find github issue URL") } + out <- core.NewExportNothing(b.Id(), "bug already exported") // will be used to mark operation related to a bug as exported bugGithubID = githubID bugGithubURL = githubURL @@ -197,33 +200,41 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { if err != nil { // if bug is still not exported and we do not have the author stop the execution - fmt.Println("warning: skipping issue due to missing token for bug creator") + // fmt.Println("warning: skipping issue due to missing token for bug creator") // this is not an error, don't export bug - return nil + out <- core.NewExportNothing(b.Id(), fmt.Sprintf("missing author token")) + return } // create bug id, url, err := createGithubIssue(client, ge.repositoryID, createOp.Title, createOp.Message) if err != nil { - return errors.Wrap(err, "exporting github issue") + err := errors.Wrap(err, "exporting github issue") + out <- core.NewExportError(err, b.Id()) + return } - // incr exported bugs - ge.exportedBugs++ + out <- core.NewExportBug(b.Id()) hash, err := createOp.Hash() if err != nil { - return errors.Wrap(err, "comment hash") + err := errors.Wrap(err, "comment hash") + out <- core.NewExportError(err, b.Id()) + return } // mark bug creation operation as exported if err := markOperationAsExported(b, hash, id, url); err != nil { - return errors.Wrap(err, "marking operation as exported") + err := errors.Wrap(err, "marking operation as exported") + out <- core.NewExportError(err, b.Id()) + return } // commit operation to avoid creating multiple issues with multiple pushes if err := b.CommitAsNeeded(); err != nil { - return errors.Wrap(err, "bug commit") + err := errors.Wrap(err, "bug commit") + out <- core.NewExportError(err, b.Id()) + return } // cache bug github ID and URL @@ -234,7 +245,8 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // get createOp hash hash, err := createOp.Hash() if err != nil { - return err + out <- core.NewExportError(err, b.Id()) + return } bugCreationHash = hash.String() @@ -251,20 +263,23 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // get operation hash hash, err := op.Hash() if err != nil { - return errors.Wrap(err, "operation hash") + err := errors.Wrap(err, "operation hash") + out <- core.NewExportError(err, b.Id()) + return } // ignore imported (or exported) operations from github // cache the ID of already exported or imported issues and events from Github if id, ok := op.GetMetadata(keyGithubId); ok { ge.cachedIDs[hash.String()] = id + out <- core.NewExportNothing(hash.String(), "already exported operation") continue } opAuthor := op.GetAuthor() client, err := ge.getIdentityClient(opAuthor.Id()) if err != nil { - // don't export operation + out <- core.NewExportNothing(hash.String(), "missing operation author token") continue } @@ -276,9 +291,13 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // send operation to github id, url, err = addCommentGithubIssue(client, bugGithubID, opr.Message) if err != nil { - return errors.Wrap(err, "adding comment") + err := errors.Wrap(err, "adding comment") + out <- core.NewExportError(err, b.Id()) + return } + out <- core.NewExportComment(hash.String()) + // cache comment id ge.cachedIDs[hash.String()] = id @@ -292,9 +311,13 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // case bug creation operation: we need to edit the Github issue if err := updateGithubIssueBody(client, bugGithubID, opr.Message); err != nil { - return errors.Wrap(err, "editing issue") + err := errors.Wrap(err, "editing issue") + out <- core.NewExportError(err, b.Id()) + return } + out <- core.NewExportCommentEdition(hash.String()) + id = bugGithubID url = bugGithubURL @@ -308,9 +331,13 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { eid, eurl, err := editCommentGithubIssue(client, commentID, opr.Message) if err != nil { - return errors.Wrap(err, "editing comment") + err := errors.Wrap(err, "editing comment") + out <- core.NewExportError(err, b.Id()) + return } + out <- core.NewExportCommentEdition(hash.String()) + // use comment id/url instead of issue id/url id = eid url = eurl @@ -319,27 +346,39 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { case *bug.SetStatusOperation: opr := op.(*bug.SetStatusOperation) if err := updateGithubIssueStatus(client, bugGithubID, opr.Status); err != nil { - return errors.Wrap(err, "editing status") + err := errors.Wrap(err, "editing status") + out <- core.NewExportError(err, b.Id()) + return } + out <- core.NewExportStatusChange(hash.String()) + id = bugGithubID url = bugGithubURL case *bug.SetTitleOperation: opr := op.(*bug.SetTitleOperation) if err := updateGithubIssueTitle(client, bugGithubID, opr.Title); err != nil { - return errors.Wrap(err, "editing title") + err := errors.Wrap(err, "editing title") + out <- core.NewExportError(err, b.Id()) + return } + out <- core.NewExportTitleEdition(hash.String()) + id = bugGithubID url = bugGithubURL case *bug.LabelChangeOperation: opr := op.(*bug.LabelChangeOperation) if err := ge.updateGithubIssueLabels(client, bugGithubID, opr.Added, opr.Removed); err != nil { - return errors.Wrap(err, "updating labels") + err := errors.Wrap(err, "updating labels") + out <- core.NewExportError(err, b.Id()) + return } + out <- core.NewExportLabelChange(hash.String()) + id = bugGithubID url = bugGithubURL @@ -349,16 +388,18 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time) error { // mark operation as exported if err := markOperationAsExported(b, hash, id, url); err != nil { - return errors.Wrap(err, "marking operation as exported") + err := errors.Wrap(err, "marking operation as exported") + out <- core.NewExportError(err, b.Id()) + return } // commit at each operation export to avoid exporting same events multiple times if err := b.CommitAsNeeded(); err != nil { - return errors.Wrap(err, "bug commit") + err := errors.Wrap(err, "bug commit") + out <- core.NewExportError(err, b.Id()) + return } } - - return nil } // getRepositoryNodeID request github api v3 to get repository node id @@ -391,7 +432,10 @@ func getRepositoryNodeID(owner, project, token string) (string, error) { }{} data, _ := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() + err = resp.Body.Close() + if err != nil { + return "", err + } err = json.Unmarshal(data, &aux) if err != nil { @@ -534,7 +578,6 @@ func (ge *githubExporter) getOrCreateGithubLabelID(gc *githubv4.Client, reposito return "", err } - ge.exportedLabels++ return labelID, nil } diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index ff8b70b8a053782285be0ae55ddf9d463ac6cfda..827152b71d406118119615fee4d1f9079913f20c 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -163,7 +163,7 @@ func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCas func TestPushPull(t *testing.T) { // repo owner - user := os.Getenv("TEST_USER") + user := os.Getenv("GITHUB_TEST_USER") // token must have 'repo' and 'delete_repo' scopes token := os.Getenv("GITHUB_TOKEN_ADMIN") @@ -180,30 +180,24 @@ func TestPushPull(t *testing.T) { // set author identity author, err := backend.NewIdentity("test identity", "test@test.org") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) err = backend.SetUserIdentity(author) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) defer backend.Close() interrupt.RegisterCleaner(backend.Close) tests, err := testCases(backend, author) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // generate project name projectName := generateRepoName() // create target Github repository - if err := createRepository(projectName, token); err != nil { - t.Fatal(err) - } + err = createRepository(projectName, token) + require.NoError(t, err) + fmt.Println("created repository", projectName) // Make sure to remove the Github repository when the test end @@ -230,7 +224,9 @@ func TestPushPull(t *testing.T) { start := time.Now() // export all bugs - err = exporter.ExportAll(backend, time.Time{}) + for result := range exporter.ExportAll(backend, time.Time{}) { + require.NoError(t, result.Err) + } require.NoError(t, err) fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds()) diff --git a/commands/bridge_push.go b/commands/bridge_push.go index 8cae1684c91157c89bba6ef7b622f16a9a7e9154..11f5ca82bbaf5e6fd9dfd46aa3b22c9d207bd7cc 100644 --- a/commands/bridge_push.go +++ b/commands/bridge_push.go @@ -1,6 +1,7 @@ package commands import ( + "fmt" "time" "github.com/spf13/cobra" @@ -32,11 +33,19 @@ func runBridgePush(cmd *cobra.Command, args []string) error { } // TODO: by default export only new events - err = b.ExportAll(time.Time{}) + out, err := b.ExportAll(time.Time{}) if err != nil { return err } + for result := range out { + if result.Err != nil { + fmt.Println(result.Err, result.Reason) + } else { + fmt.Printf("%s: %s\n", result.String(), result.ID) + } + } + return nil } From 4b6949fcce9ea5caf8c62274c4441f41827dc854 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Thu, 27 Jun 2019 00:42:44 +0200 Subject: [PATCH 25/29] Add exporter documentation --- README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8449aaed0d39a32bc8466a85feeaa020d2947d13..8e14cebb920dbd3140ed79326bda349bc9ce6939 100644 --- a/README.md +++ b/README.md @@ -116,23 +116,31 @@ The web UI interact with the backend through a GraphQL API. The schema is availa ### Importer implementations -| | Github | Launchpad | -| ----------------------------------------------- | :----------------: | :----------------: | -| **incremental**
(can import more than once) | :heavy_check_mark: | :x: | -| **with resume**
(download only new data) | :x: | :x: | -| **identities** | :heavy_check_mark: | :heavy_check_mark: | -| identities update | :x: | :x: | -| **bug** | :heavy_check_mark: | :heavy_check_mark: | -| comments | :heavy_check_mark: | :heavy_check_mark: | -| comment editions | :heavy_check_mark: | :x: | -| labels | :heavy_check_mark: | :x: | -| status | :heavy_check_mark: | :x: | -| title edition | :heavy_check_mark: | :x: | -| **automated test suite** | :x: | :x: | +| | Github | Launchpad | +| --- | --- | --- | +| **incremental**
(can import more than once) | :heavy_check_mark: | :x: | +| **with resume**
(download only new data) | :x: | :x: | +| **identities** | :heavy_check_mark: | :heavy_check_mark: | +| identities update | :x: | :x: | +| **bug** | :heavy_check_mark: | :heavy_check_mark: | +| comments | :heavy_check_mark: | :heavy_check_mark: | +| comment editions | :heavy_check_mark: | :x: | +| labels | :heavy_check_mark: | :x: | +| status | :heavy_check_mark: | :x: | +| title edition | :heavy_check_mark: | :x: | +| **automated test suite** | :heavy_check_mark: | :x: | ### Exporter implementations -Todo ! +| | Github | Launchpad | +| --- | --- | --- | +| **bug** | :heavy_check_mark: | :x: | +| comments | :heavy_check_mark: | :x: | +| comment editions | :heavy_check_mark: | :x: | +| labels | :heavy_check_mark: | :x: | +| status | :heavy_check_mark: | :x: | +| title edition | :heavy_check_mark: | :x: | +| **automated test suite** | :heavy_check_mark: | :x: | ## Internals From 93e731fd0d365c0cf49dbb7aea371e48f46e1f11 Mon Sep 17 00:00:00 2001 From: Amine Date: Fri, 5 Jul 2019 18:32:51 +0200 Subject: [PATCH 26/29] [bridge/github] improve comments and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [bridge/github] improve error handling and tests Co-Authored-By: Michael Muré --- bridge/core/bridge.go | 2 +- bridge/core/interfaces.go | 2 +- bridge/github/export.go | 77 ++++++++++++++++--------------- bridge/github/export_test.go | 89 +++++++++++------------------------- 4 files changed, 69 insertions(+), 101 deletions(-) diff --git a/bridge/core/bridge.go b/bridge/core/bridge.go index 30e051be338a9cb24651cce76c261a9f845270b9..d04e88e40d563c68bf1829cecb016e7448a70226 100644 --- a/bridge/core/bridge.go +++ b/bridge/core/bridge.go @@ -313,5 +313,5 @@ func (b *Bridge) ExportAll(since time.Time) (<-chan ExportResult, error) { return nil, err } - return exporter.ExportAll(b.repo, since), nil + return exporter.ExportAll(b.repo, since) } diff --git a/bridge/core/interfaces.go b/bridge/core/interfaces.go index 5cc7006a168f5b1d83097354c9f2a8ee243b9e7d..76d66fb4e887fadec2351e8de05844e9d16bd922 100644 --- a/bridge/core/interfaces.go +++ b/bridge/core/interfaces.go @@ -34,5 +34,5 @@ type Importer interface { type Exporter interface { Init(conf Configuration) error - ExportAll(repo *cache.RepoCache, since time.Time) <-chan ExportResult + ExportAll(repo *cache.RepoCache, since time.Time) (<-chan ExportResult, error) } diff --git a/bridge/github/export.go b/bridge/github/export.go index 40044ca11b54ab13ef1060ffc3e7ad4369d9480c..5e5b1a21b509894b13271e8b72832ce4ffa3a1b2 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -40,7 +40,7 @@ type githubExporter struct { // cache identifiers used to speed up exporting operations // cleared for each bug - cachedIDs map[string]string + cachedOperationIDs map[string]string // cache labels used to speed up exporting labels events cachedLabels map[string]string @@ -52,7 +52,7 @@ func (ge *githubExporter) Init(conf core.Configuration) error { //TODO: initialize with multiple tokens ge.identityToken = make(map[string]string) ge.identityClient = make(map[string]*githubv4.Client) - ge.cachedIDs = make(map[string]string) + ge.cachedOperationIDs = make(map[string]string) ge.cachedLabels = make(map[string]string) return nil } @@ -74,7 +74,7 @@ func (ge *githubExporter) allowOrigin(origin string) bool { return false } -// getIdentityClient return an identity github api v4 client +// getIdentityClient return a githubv4 API client configured with the access token of the given identity. // if no client were found it will initialize it from the known tokens map and cache it for next use func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) { client, ok := ge.identityClient[id] @@ -97,31 +97,29 @@ func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) } // ExportAll export all event made by the current user to Github -func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) <-chan core.ExportResult { +func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) { out := make(chan core.ExportResult) - go func(out chan<- core.ExportResult) { - defer close(out) + user, err := repo.GetUserIdentity() + if err != nil { + return nil, err + } - user, err := repo.GetUserIdentity() - if err != nil { - out <- core.NewExportError(err, "") - return - } + ge.identityToken[user.Id()] = ge.conf[keyToken] - ge.identityToken[user.Id()] = ge.conf[keyToken] + // get repository node id + ge.repositoryID, err = getRepositoryNodeID( + ge.conf[keyOwner], + ge.conf[keyProject], + ge.conf[keyToken], + ) - // get repository node id - ge.repositoryID, err = getRepositoryNodeID( - ge.conf[keyOwner], - ge.conf[keyProject], - ge.conf[keyToken], - ) + if err != nil { + return nil, err + } - if err != nil { - out <- core.NewExportError(err, ge.repositoryID) - return - } + go func() { + defer close(out) var allIdentitiesIds []string for id := range ge.identityToken { @@ -140,6 +138,7 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) <-ch snapshot := b.Snapshot() // ignore issues created before since date + // TODO: compare the Lamport time instead of using the unix time if snapshot.CreatedAt.Before(since) { out <- core.NewExportNothing(b.Id(), "bug created before the since date") continue @@ -152,9 +151,9 @@ func (ge *githubExporter) ExportAll(repo *cache.RepoCache, since time.Time) <-ch out <- core.NewExportNothing(id, "not an actor") } } - }(out) + }() - return out + return out, nil } // exportBug publish bugs and related events @@ -176,7 +175,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan // skip bug if origin is not allowed origin, ok := createOp.GetMetadata(keyOrigin) if ok && !ge.allowOrigin(origin) { - out <- core.NewExportNothing(b.Id(), fmt.Sprintf("issue taged with origin: %s", origin)) + out <- core.NewExportNothing(b.Id(), fmt.Sprintf("issue tagged with origin: %s", origin)) return } @@ -186,7 +185,8 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan githubURL, ok := createOp.GetMetadata(keyGithubUrl) if !ok { // if we find github ID, github URL must be found too - panic("expected to find github issue URL") + err := fmt.Errorf("expected to find github issue URL") + out <- core.NewExportError(err, b.Id()) } out <- core.NewExportNothing(b.Id(), "bug already exported") @@ -199,9 +199,6 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan client, err := ge.getIdentityClient(author.Id()) if err != nil { // if bug is still not exported and we do not have the author stop the execution - - // fmt.Println("warning: skipping issue due to missing token for bug creator") - // this is not an error, don't export bug out <- core.NewExportNothing(b.Id(), fmt.Sprintf("missing author token")) return } @@ -252,7 +249,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan bugCreationHash = hash.String() // cache operation github id - ge.cachedIDs[bugCreationHash] = bugGithubID + ge.cachedOperationIDs[bugCreationHash] = bugGithubID for _, op := range snapshot.Operations[1:] { // ignore SetMetadata operations @@ -268,10 +265,10 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan return } - // ignore imported (or exported) operations from github + // ignore operations already existing in github (due to import or export) // cache the ID of already exported or imported issues and events from Github if id, ok := op.GetMetadata(keyGithubId); ok { - ge.cachedIDs[hash.String()] = id + ge.cachedOperationIDs[hash.String()] = id out <- core.NewExportNothing(hash.String(), "already exported operation") continue } @@ -299,7 +296,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan out <- core.NewExportComment(hash.String()) // cache comment id - ge.cachedIDs[hash.String()] = id + ge.cachedOperationIDs[hash.String()] = id case *bug.EditCommentOperation: @@ -324,7 +321,7 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan } else { // case comment edition operation: we need to edit the Github comment - commentID, ok := ge.cachedIDs[targetHash] + commentID, ok := ge.cachedOperationIDs[targetHash] if !ok { panic("unexpected error: comment id not found") } @@ -424,7 +421,7 @@ func getRepositoryNodeID(owner, project, token string) (string, error) { } if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("error retrieving repository node id %v", resp.StatusCode) + return "", fmt.Errorf("HTTP error %v retrieving repository node id", resp.StatusCode) } aux := struct { @@ -668,9 +665,15 @@ func updateGithubIssueStatus(gc *githubv4.Client, id string, status bug.Status) m := &updateIssueMutation{} // set state - state := githubv4.IssueStateClosed - if status == bug.OpenStatus { + var state githubv4.IssueState + + switch status { + case bug.OpenStatus: + state = githubv4.IssueStateOpen + case bug.ClosedStatus: state = githubv4.IssueStateOpen + default: + panic("unknown bug state") } input := githubv4.UpdateIssueInput{ diff --git a/bridge/github/export_test.go b/bridge/github/export_test.go index 827152b71d406118119615fee4d1f9079913f20c..80660e774beb4ebf1b232d4c0f91ff965ee7f20b 100644 --- a/bridge/github/export_test.go +++ b/bridge/github/export_test.go @@ -29,102 +29,66 @@ type testCase struct { numOrOp int // number of original operations } -func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCase, error) { +func testCases(t *testing.T, repo *cache.RepoCache, identity *cache.IdentityCache) []*testCase { // simple bug simpleBug, _, err := repo.NewBug("simple bug", "new bug") - if err != nil { - return nil, err - } + require.NoError(t, err) // bug with comments bugWithComments, _, err := repo.NewBug("bug with comments", "new bug") - if err != nil { - return nil, err - } + require.NoError(t, err) _, err = bugWithComments.AddComment("new comment") - if err != nil { - return nil, err - } + require.NoError(t, err) // bug with label changes bugLabelChange, _, err := repo.NewBug("bug label change", "new bug") - if err != nil { - return nil, err - } + require.NoError(t, err) _, _, err = bugLabelChange.ChangeLabels([]string{"bug"}, nil) - if err != nil { - return nil, err - } + require.NoError(t, err) _, _, err = bugLabelChange.ChangeLabels([]string{"core"}, nil) - if err != nil { - return nil, err - } + require.NoError(t, err) _, _, err = bugLabelChange.ChangeLabels(nil, []string{"bug"}) - if err != nil { - return nil, err - } + require.NoError(t, err) // bug with comments editions bugWithCommentEditions, createOp, err := repo.NewBug("bug with comments editions", "new bug") - if err != nil { - return nil, err - } + require.NoError(t, err) createOpHash, err := createOp.Hash() - if err != nil { - return nil, err - } + require.NoError(t, err) _, err = bugWithCommentEditions.EditComment(createOpHash, "first comment edited") - if err != nil { - return nil, err - } + require.NoError(t, err) commentOp, err := bugWithCommentEditions.AddComment("first comment") - if err != nil { - return nil, err - } + require.NoError(t, err) commentOpHash, err := commentOp.Hash() - if err != nil { - return nil, err - } + require.NoError(t, err) _, err = bugWithCommentEditions.EditComment(commentOpHash, "first comment edited") - if err != nil { - return nil, err - } + require.NoError(t, err) // bug status changed bugStatusChanged, _, err := repo.NewBug("bug status changed", "new bug") - if err != nil { - return nil, err - } + require.NoError(t, err) _, err = bugStatusChanged.Close() - if err != nil { - return nil, err - } + require.NoError(t, err) _, err = bugStatusChanged.Open() - if err != nil { - return nil, err - } + require.NoError(t, err) // bug title changed bugTitleEdited, _, err := repo.NewBug("bug title edited", "new bug") - if err != nil { - return nil, err - } + require.NoError(t, err) _, err = bugTitleEdited.SetTitle("bug title edited again") - if err != nil { - return nil, err - } + require.NoError(t, err) return []*testCase{ &testCase{ @@ -157,8 +121,7 @@ func testCases(repo *cache.RepoCache, identity *cache.IdentityCache) ([]*testCas bug: bugTitleEdited, numOrOp: 2, }, - }, nil - + } } func TestPushPull(t *testing.T) { @@ -188,8 +151,7 @@ func TestPushPull(t *testing.T) { defer backend.Close() interrupt.RegisterCleaner(backend.Close) - tests, err := testCases(backend, author) - require.NoError(t, err) + tests := testCases(t, backend, author) // generate project name projectName := generateRepoName() @@ -224,7 +186,10 @@ func TestPushPull(t *testing.T) { start := time.Now() // export all bugs - for result := range exporter.ExportAll(backend, time.Time{}) { + events, err := exporter.ExportAll(backend, time.Time{}) + require.NoError(t, err) + + for result := range events { require.NoError(t, result.Err) } require.NoError(t, err) @@ -258,7 +223,7 @@ func TestPushPull(t *testing.T) { // so number of operations should double require.Len(t, tt.bug.Snapshot().Operations, tt.numOrOp*2) - // verify operation have correcte metadata + // verify operation have correct metadata for _, op := range tt.bug.Snapshot().Operations { // Check if the originals operations (*not* SetMetadata) are tagged properly if _, ok := op.(*bug.SetMetadataOperation); !ok { @@ -274,7 +239,7 @@ func TestPushPull(t *testing.T) { bugGithubID, ok := tt.bug.Snapshot().GetCreateMetadata(keyGithubId) require.True(t, ok) - // retrive bug from backendTwo + // retrieve bug from backendTwo importedBug, err := backendTwo.ResolveBugCreateMetadata(keyGithubId, bugGithubID) require.NoError(t, err) From 3e181168eadb1d6a13e67583c16219200d052fcc Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Fri, 5 Jul 2019 20:55:24 +0200 Subject: [PATCH 27/29] [bridge/github] improve export label change function [bridge/core] rename `EventStatus` to `ExportEvent` --- bridge/core/export.go | 48 +++++++++++----------- bridge/github/export.go | 91 ++++++++++++++++++++++++++--------------- 2 files changed, 81 insertions(+), 58 deletions(-) diff --git a/bridge/core/export.go b/bridge/core/export.go index 5114943025b9b674e756988348b3956af386d083..080ced803574e6ae97219a3c24aef90b9b9f27ff 100644 --- a/bridge/core/export.go +++ b/bridge/core/export.go @@ -2,41 +2,41 @@ package core import "fmt" -type EventStatus int +type ExportEvent int const ( - _ EventStatus = iota - EventStatusBug - EventStatusComment - EventStatusCommentEdition - EventStatusStatusChange - EventStatusTitleEdition - EventStatusLabelChange - EventStatusNothing + _ ExportEvent = iota + ExportEventBug + ExportEventComment + ExportEventCommentEdition + ExportEventStatusChange + ExportEventTitleEdition + ExportEventLabelChange + ExportEventNothing ) type ExportResult struct { Err error - Event EventStatus + Event ExportEvent ID string Reason string } func (er ExportResult) String() string { switch er.Event { - case EventStatusBug: + case ExportEventBug: return "new issue" - case EventStatusComment: + case ExportEventComment: return "new comment" - case EventStatusCommentEdition: + case ExportEventCommentEdition: return "updated comment" - case EventStatusStatusChange: + case ExportEventStatusChange: return "changed status" - case EventStatusTitleEdition: + case ExportEventTitleEdition: return "changed title" - case EventStatusLabelChange: + case ExportEventLabelChange: return "changed label" - case EventStatusNothing: + case ExportEventNothing: return fmt.Sprintf("no event: %v", er.Reason) default: panic("unknown export result") @@ -54,48 +54,48 @@ func NewExportNothing(id string, reason string) ExportResult { return ExportResult{ ID: id, Reason: reason, - Event: EventStatusNothing, + Event: ExportEventNothing, } } func NewExportBug(id string) ExportResult { return ExportResult{ ID: id, - Event: EventStatusBug, + Event: ExportEventBug, } } func NewExportComment(id string) ExportResult { return ExportResult{ ID: id, - Event: EventStatusComment, + Event: ExportEventComment, } } func NewExportCommentEdition(id string) ExportResult { return ExportResult{ ID: id, - Event: EventStatusCommentEdition, + Event: ExportEventCommentEdition, } } func NewExportStatusChange(id string) ExportResult { return ExportResult{ ID: id, - Event: EventStatusStatusChange, + Event: ExportEventStatusChange, } } func NewExportLabelChange(id string) ExportResult { return ExportResult{ ID: id, - Event: EventStatusLabelChange, + Event: ExportEventLabelChange, } } func NewExportTitleEdition(id string) ExportResult { return ExportResult{ ID: id, - Event: EventStatusTitleEdition, + Event: ExportEventTitleEdition, } } diff --git a/bridge/github/export.go b/bridge/github/export.go index 5e5b1a21b509894b13271e8b72832ce4ffa3a1b2..c4ca42dab9aa7ebef702dc29e5b063f54775de64 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -7,6 +7,8 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" + "sync" "time" "github.com/pkg/errors" @@ -671,7 +673,7 @@ func updateGithubIssueStatus(gc *githubv4.Client, id string, status bug.Status) case bug.OpenStatus: state = githubv4.IssueStateOpen case bug.ClosedStatus: - state = githubv4.IssueStateOpen + state = githubv4.IssueStateClosed default: panic("unknown bug state") } @@ -730,49 +732,70 @@ func updateGithubIssueTitle(gc *githubv4.Client, id, title string) error { // update github issue labels func (ge *githubExporter) updateGithubIssueLabels(gc *githubv4.Client, labelableID string, added, removed []bug.Label) error { - addedIDs, err := ge.getLabelsIDs(gc, labelableID, added) - if err != nil { - return errors.Wrap(err, "getting added labels ids") - } - - m := &addLabelsToLabelableMutation{} - inputAdd := githubv4.AddLabelsToLabelableInput{ - LabelableID: labelableID, - LabelIDs: addedIDs, - } + var errs []string + var wg sync.WaitGroup parentCtx := context.Background() - ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) - // add labels - if err := gc.Mutate(ctx, m, inputAdd, nil); err != nil { - cancel() - return err - } - cancel() + if len(added) > 0 { + wg.Add(1) + go func() { + defer wg.Done() - if len(removed) == 0 { - return nil - } + addedIDs, err := ge.getLabelsIDs(gc, labelableID, added) + if err != nil { + errs = append(errs, errors.Wrap(err, "getting added labels ids").Error()) + return + } - removedIDs, err := ge.getLabelsIDs(gc, labelableID, removed) - if err != nil { - return errors.Wrap(err, "getting added labels ids") + m := &addLabelsToLabelableMutation{} + inputAdd := githubv4.AddLabelsToLabelableInput{ + LabelableID: labelableID, + LabelIDs: addedIDs, + } + + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + // add labels + if err := gc.Mutate(ctx, m, inputAdd, nil); err != nil { + errs = append(errs, err.Error()) + } + }() } - m2 := &removeLabelsFromLabelableMutation{} - inputRemove := githubv4.RemoveLabelsFromLabelableInput{ - LabelableID: labelableID, - LabelIDs: removedIDs, + if len(removed) > 0 { + wg.Add(1) + go func() { + defer wg.Done() + + removedIDs, err := ge.getLabelsIDs(gc, labelableID, removed) + if err != nil { + errs = append(errs, errors.Wrap(err, "getting added labels ids").Error()) + return + } + + m2 := &removeLabelsFromLabelableMutation{} + inputRemove := githubv4.RemoveLabelsFromLabelableInput{ + LabelableID: labelableID, + LabelIDs: removedIDs, + } + + ctx, cancel := context.WithTimeout(parentCtx, defaultTimeout) + defer cancel() + + // remove label labels + if err := gc.Mutate(ctx, m2, inputRemove, nil); err != nil { + errs = append(errs, err.Error()) + } + }() } - ctx, cancel = context.WithTimeout(parentCtx, defaultTimeout) - defer cancel() + wg.Wait() - // remove label labels - if err := gc.Mutate(ctx, m2, inputRemove, nil); err != nil { - return err + if len(errs) == 0 { + return nil } - return nil + return fmt.Errorf("label change error: %v", strings.Join(errs, "\n")) } From 6323c7b10663024f03d6c8c641852c702a5d8a04 Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 6 Jul 2019 00:23:19 +0200 Subject: [PATCH 28/29] [bridge/github] ignore imported bugs from other repositories --- bridge/github/export.go | 42 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/bridge/github/export.go b/bridge/github/export.go index c4ca42dab9aa7ebef702dc29e5b063f54775de64..b4351bdb437490c3188f5c9464f3f0ac5a0dc3fb 100644 --- a/bridge/github/export.go +++ b/bridge/github/export.go @@ -28,9 +28,6 @@ var ( type githubExporter struct { conf core.Configuration - // export only bugs tagged with one of these origins - onlyOrigins []string - // cache identities clients identityClient map[string]*githubv4.Client @@ -59,23 +56,6 @@ func (ge *githubExporter) Init(conf core.Configuration) error { return nil } -// allowOrigin verify that origin is allowed to get exported. -// if the exporter was initialized with no specified origins, it will return -// true for all origins -func (ge *githubExporter) allowOrigin(origin string) bool { - if len(ge.onlyOrigins) == 0 { - return true - } - - for _, o := range ge.onlyOrigins { - if origin == o { - return true - } - } - - return false -} - // getIdentityClient return a githubv4 API client configured with the access token of the given identity. // if no client were found it will initialize it from the known tokens map and cache it for next use func (ge *githubExporter) getIdentityClient(id string) (*githubv4.Client, error) { @@ -175,22 +155,36 @@ func (ge *githubExporter) exportBug(b *cache.BugCache, since time.Time, out chan author := snapshot.Author // skip bug if origin is not allowed - origin, ok := createOp.GetMetadata(keyOrigin) - if ok && !ge.allowOrigin(origin) { + origin, ok := snapshot.GetCreateMetadata(keyOrigin) + if ok && origin != target { out <- core.NewExportNothing(b.Id(), fmt.Sprintf("issue tagged with origin: %s", origin)) return } // get github bug ID - githubID, ok := createOp.GetMetadata(keyGithubId) + githubID, ok := snapshot.GetCreateMetadata(keyGithubId) if ok { - githubURL, ok := createOp.GetMetadata(keyGithubUrl) + githubURL, ok := snapshot.GetCreateMetadata(keyGithubUrl) if !ok { // if we find github ID, github URL must be found too err := fmt.Errorf("expected to find github issue URL") out <- core.NewExportError(err, b.Id()) } + // extract owner and project + owner, project, err := splitURL(githubURL) + if err != nil { + err := fmt.Errorf("bad project url: %v", err) + out <- core.NewExportError(err, b.Id()) + return + } + + // ignore issue comming from other repositories + if owner != ge.conf[keyOwner] && project != ge.conf[keyProject] { + out <- core.NewExportNothing(b.Id(), fmt.Sprintf("skipping issue from url:%s", githubURL)) + return + } + out <- core.NewExportNothing(b.Id(), "bug already exported") // will be used to mark operation related to a bug as exported bugGithubID = githubID From 9e611ee66787b9f005540395da2ea10b3320362c Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Sat, 6 Jul 2019 11:46:59 +0200 Subject: [PATCH 29/29] [bridge/github] import: avoid retrieving ghost email in userQuery request rename userQuery to ghostQuery --- bridge/github/import.go | 4 ++-- bridge/github/import_query.go | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bridge/github/import.go b/bridge/github/import.go index e87c6b4b04be8919c414b8e9ef82cbc99288510b..c162d70e2239caaa26ef4cfc5977057aeb00bde6 100644 --- a/bridge/github/import.go +++ b/bridge/github/import.go @@ -505,7 +505,7 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, return nil, err } - var q userQuery + var q ghostQuery variables := map[string]interface{}{ "login": githubv4.String("ghost"), @@ -529,7 +529,7 @@ func (gi *githubImporter) getGhost(repo *cache.RepoCache) (*cache.IdentityCache, return repo.NewIdentityRaw( name, - string(q.User.Email), + "", string(q.User.Login), string(q.User.AvatarUrl), map[string]string{ diff --git a/bridge/github/import_query.go b/bridge/github/import_query.go index 34931c7d53bc98fc9206937a91e6ff8ff4f5bca4..89d7859d0c460669de5495cc75fedd14b917f7a1 100644 --- a/bridge/github/import_query.go +++ b/bridge/github/import_query.go @@ -160,12 +160,11 @@ type commentEditQuery struct { } `graphql:"repository(owner: $owner, name: $name)"` } -type userQuery struct { +type ghostQuery struct { User struct { Login githubv4.String AvatarUrl githubv4.String Name *githubv4.String - Email githubv4.String } `graphql:"user(login: $login)"` }