mirror of
https://github.com/mitsuhiko/gh-issue-sync.git
synced 2026-09-14 20:26:44 +08:00
Make sync system more resilient to drift
This commit is contained in:
@@ -58,6 +58,7 @@ type PushCommand struct {
|
||||
BaseCommand
|
||||
DryRun bool `long:"dry-run" description:"Show what would happen without pushing"`
|
||||
NoComments bool `long:"no-comments" description:"Skip posting pending comments"`
|
||||
Force bool `long:"force" description:"Skip conflict detection and push anyway"`
|
||||
Args struct {
|
||||
Issues []string `positional-arg-name:"issue" description:"Issue numbers, local IDs, or paths to push"`
|
||||
} `positional-args:"yes"`
|
||||
@@ -207,7 +208,7 @@ func (c *PullCommand) Execute(args []string) error {
|
||||
}
|
||||
|
||||
func (c *PushCommand) Execute(args []string) error {
|
||||
opts := app.PushOptions{DryRun: c.DryRun, NoComments: c.NoComments}
|
||||
opts := app.PushOptions{DryRun: c.DryRun, NoComments: c.NoComments, Force: c.Force}
|
||||
if len(c.Args.Issues) > 0 {
|
||||
return c.App.Push(context.Background(), opts, c.Args.Issues)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type PullOptions struct {
|
||||
type PushOptions struct {
|
||||
DryRun bool
|
||||
NoComments bool
|
||||
Force bool
|
||||
}
|
||||
|
||||
type NewOptions struct {
|
||||
|
||||
+17
-3
@@ -454,9 +454,23 @@ func (a *App) Push(ctx context.Context, opts PushOptions, args []string) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if pu.HasOriginal && !issue.EqualForConflictCheck(remote, pu.Original) {
|
||||
conflicts = append(conflicts, numStr)
|
||||
conflictCount++
|
||||
if !opts.Force && pu.HasOriginal && !issue.EqualForConflictCheck(remote, pu.Original) {
|
||||
// Remote changed since last sync, but check if local matches remote
|
||||
// (i.e., the same change was already applied - no real conflict)
|
||||
if !issue.EqualForConflictCheck(remote, pu.Item.Issue) {
|
||||
conflicts = append(conflicts, numStr)
|
||||
conflictCount++
|
||||
continue
|
||||
}
|
||||
// Local matches remote - update the original and skip (nothing to push)
|
||||
if err := writeOriginalIssue(p, remote); err != nil {
|
||||
progress.Log(fmt.Sprintf("%s updating original for #%s: %v", t.WarningText("Warning:"), numStr, err))
|
||||
}
|
||||
pu.Item.Issue.SyncedAt = ptrTime(a.Now().UTC())
|
||||
if err := issue.WriteFile(pu.Item.Path, pu.Item.Issue); err != nil {
|
||||
progress.Log(fmt.Sprintf("%s updating local file for #%s: %v", t.WarningText("Warning:"), numStr, err))
|
||||
}
|
||||
unchanged++
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -550,6 +550,9 @@ func (c *Client) GetIssue(ctx context.Context, number string) (issue.Issue, erro
|
||||
return payload.ToIssue(), nil
|
||||
}
|
||||
|
||||
// batchQueryChunkSize is the maximum number of issues to query in a single GraphQL call.
|
||||
const batchQueryChunkSize = 20
|
||||
|
||||
// GetIssuesBatch fetches multiple issues in a single GraphQL call.
|
||||
// Returns a map of issue number -> issue. Issues that don't exist are not included.
|
||||
func (c *Client) GetIssuesBatch(ctx context.Context, numbers []string) (map[string]issue.Issue, error) {
|
||||
@@ -557,6 +560,34 @@ func (c *Client) GetIssuesBatch(ctx context.Context, numbers []string) (map[stri
|
||||
return map[string]issue.Issue{}, nil
|
||||
}
|
||||
|
||||
// Process in chunks to avoid GitHub's resource limits
|
||||
results := make(map[string]issue.Issue)
|
||||
for i := 0; i < len(numbers); i += batchQueryChunkSize {
|
||||
end := i + batchQueryChunkSize
|
||||
if end > len(numbers) {
|
||||
end = len(numbers)
|
||||
}
|
||||
chunk := numbers[i:end]
|
||||
|
||||
chunkResults, err := c.getIssuesBatchChunk(ctx, chunk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range chunkResults {
|
||||
results[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// getIssuesBatchChunk fetches a single chunk of issues.
|
||||
func (c *Client) getIssuesBatchChunk(ctx context.Context, numbers []string) (map[string]issue.Issue, error) {
|
||||
if len(numbers) == 0 {
|
||||
return map[string]issue.Issue{}, nil
|
||||
}
|
||||
|
||||
owner, repo := splitRepo(c.repo)
|
||||
if owner == "" || repo == "" {
|
||||
return nil, fmt.Errorf("invalid repository format")
|
||||
|
||||
@@ -88,6 +88,34 @@ func (c *Client) GetIssueRelationshipsBatch(ctx context.Context, numbers []strin
|
||||
return map[string]IssueRelationships{}, nil
|
||||
}
|
||||
|
||||
// Process in chunks to avoid GitHub's resource limits
|
||||
results := make(map[string]IssueRelationships)
|
||||
for i := 0; i < len(numbers); i += batchChunkSize {
|
||||
end := i + batchChunkSize
|
||||
if end > len(numbers) {
|
||||
end = len(numbers)
|
||||
}
|
||||
chunk := numbers[i:end]
|
||||
|
||||
chunkResults, err := c.getIssueRelationshipsBatchChunk(ctx, chunk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range chunkResults {
|
||||
results[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// getIssueRelationshipsBatchChunk fetches relationships for a single chunk of issues.
|
||||
func (c *Client) getIssueRelationshipsBatchChunk(ctx context.Context, numbers []string) (map[string]IssueRelationships, error) {
|
||||
if len(numbers) == 0 {
|
||||
return map[string]IssueRelationships{}, nil
|
||||
}
|
||||
|
||||
owner, repo := splitRepo(c.repo)
|
||||
if owner == "" || repo == "" {
|
||||
return nil, fmt.Errorf("invalid repository format")
|
||||
@@ -596,6 +624,10 @@ type BatchUpdateResult struct {
|
||||
Errors map[string]string // Issue number -> error message
|
||||
}
|
||||
|
||||
// batchChunkSize is the maximum number of issues to update in a single GraphQL call.
|
||||
// GitHub's GraphQL API has resource limits that prevent very large mutations.
|
||||
const batchChunkSize = 20
|
||||
|
||||
// BatchEditIssues updates multiple issues in a single GraphQL call.
|
||||
// This is much faster than calling EditIssue for each issue individually.
|
||||
// Note: This only handles title, body, milestone, labels, and assignees.
|
||||
@@ -609,6 +641,38 @@ func (c *Client) BatchEditIssues(ctx context.Context, updates []BatchIssueUpdate
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Process updates in chunks to avoid GitHub's resource limits
|
||||
for i := 0; i < len(updates); i += batchChunkSize {
|
||||
end := i + batchChunkSize
|
||||
if end > len(updates) {
|
||||
end = len(updates)
|
||||
}
|
||||
chunk := updates[i:end]
|
||||
|
||||
chunkResult, err := c.batchEditIssuesChunk(ctx, chunk)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.Updated = append(result.Updated, chunkResult.Updated...)
|
||||
for k, v := range chunkResult.Errors {
|
||||
result.Errors[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// batchEditIssuesChunk processes a single chunk of batch updates.
|
||||
func (c *Client) batchEditIssuesChunk(ctx context.Context, updates []BatchIssueUpdate) (BatchUpdateResult, error) {
|
||||
result := BatchUpdateResult{
|
||||
Errors: make(map[string]string),
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
owner, repo := splitRepo(c.repo)
|
||||
if owner == "" || repo == "" {
|
||||
return result, fmt.Errorf("invalid repository format")
|
||||
|
||||
Reference in New Issue
Block a user