aboutsummaryrefslogblamecommitdiffstats
path: root/bridge/gitlab/import.go
blob: cf6b5ca68f1635f39413cb0fe575f0051e9816e5 (plain) (tree)
1
2
3
4
5
6
7
8
9
10


              
                 
             
                 

              

                                    
                                                    
                                                         
                                              
                                                     
                                               
                                                  

 
                                                  


                               
                         

                             

                                    

 
                                                                                                         
                      
 


                                              
                                                                               
                                                                            
         







                                              
                                                                                        


                          
 


                  

                                                                                              
                                                                                                                                    



                                           
                                
 
                                                                                             
 





                                                                            
 




                                                                   
 
                                                    


                                                                             
                                 


                                                                                              
                                 
                         
 



                                                                                             


                                                                        
                         
                 
           
 
                       

 







                                                                                                            

                                                                               
                                                                                                  

                                                                                                        
          


                             


                                      
 
                                                


                                       

                                                 

                                  
                                                     
                                                                           
                                                           

                                                                            

                  
 

                               

         
                              
                                           
 
                     

 
                                                                                                                              
                                                                                     



                                                                     
                              
                                                            



                          

                             



                                      
                                      
                               
                                                 
                                          
                                                            

                          
 




                                                             
 
                           



                                      
                                     
                               
                                                 
                                          
                                                            

                          




                                                             
 
                                     
                                                        

                                                                                              
                                                                                                  

                                                                                               
                                          
                                                    
                                       
                                                                   

                                                          
                                                  
                                                                    

                                  


                                          
 
                                                                     

                 

                                                                 

                                                  
                                                        

                                                
                                                   
                                       
                                                         


                                                  
                                                                    

                                  




                                                                



                                                  
                                                 
                                                                    



                                  
                                                                    
                                                 
                                          
                                                    
                                       
                                                                   
                                                     
                                          
                                    

                         



                                                                       



                          
                               
                                                          



                                      
                                         
                               

                                                  
                                          
                                                            

                          


                                  
 
                                                             
 
                           

                                                

                                                                

                                          
                                                            

                          
                          
 
                              

                                                
                                                 
                            
                                                                
                                          
                                                            

                          













                                             

                
                                                     

         
                  


                                                                                                     
                                  
                                                                                          


                             
                                           


                               
                                                                             



                               
                                     

                                 
                              
                               
                    
                                  
                                         

                                                             

                  





                                                
 
package gitlab

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/xanzy/go-gitlab"

	"github.com/MichaelMure/git-bug/bridge/core"
	"github.com/MichaelMure/git-bug/bridge/core/auth"
	"github.com/MichaelMure/git-bug/cache"
	"github.com/MichaelMure/git-bug/entities/bug"
	"github.com/MichaelMure/git-bug/entity"
	"github.com/MichaelMure/git-bug/util/text"
)

// gitlabImporter implement the Importer interface
type gitlabImporter struct {
	conf core.Configuration

	// default client
	client *gitlab.Client

	// send only channel
	out chan<- core.ImportResult
}

func (gi *gitlabImporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
	gi.conf = conf

	creds, err := auth.List(repo,
		auth.WithTarget(target),
		auth.WithKind(auth.KindToken),
		auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyGitlabBaseUrl]),
		auth.WithMeta(auth.MetaKeyLogin, conf[confKeyDefaultLogin]),
	)
	if err != nil {
		return err
	}

	if len(creds) == 0 {
		return ErrMissingIdentityToken
	}

	gi.client, err = buildClient(conf[confKeyGitlabBaseUrl], creds[0].(*auth.Token))
	if err != nil {
		return err
	}

	return nil
}

// ImportAll iterate over all the configured repository issues (notes) and ensure the creation
// of the missing issues / comments / label events / title changes ...
func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
	out := make(chan core.ImportResult)
	gi.out = out

	go func() {
		defer close(out)

		for issue := range Issues(ctx, gi.client, gi.conf[confKeyProjectID], since) {

			b, err := gi.ensureIssue(repo, issue)
			if err != nil {
				err := fmt.Errorf("issue creation: %v", err)
				out <- core.NewImportError(err, "")
				return
			}

			issueEvents := SortedEvents(
				Notes(ctx, gi.client, issue),
				LabelEvents(ctx, gi.client, issue),
				StateEvents(ctx, gi.client, issue),
			)

			for e := range issueEvents {
				if e, ok := e.(ErrorEvent); ok {
					out <- core.NewImportError(e.Err, "")
					continue
				}
				if err := gi.ensureIssueEvent(repo, b, issue, e); err != nil {
					err := fmt.Errorf("issue event creation: %v", err)
					out <- core.NewImportError(err, entity.Id(e.ID()))
				}
			}

			if !b.NeedCommit() {
				out <- core.NewImportNothing(b.Id(), "no imported operation")
			} else if err := b.Commit(); err != nil {
				// commit bug state
				err := fmt.Errorf("bug commit: %v", err)
				out <- core.NewImportError(err, "")
				return
			}
		}
	}()

	return out, nil
}

func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue) (*cache.BugCache, error) {
	// ensure issue author
	author, err := gi.ensurePerson(repo, issue.Author.ID)
	if err != nil {
		return nil, err
	}

	// resolve bug
	b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
		return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
			excerpt.CreateMetadata[metaKeyGitlabId] == fmt.Sprintf("%d", issue.IID) &&
			excerpt.CreateMetadata[metaKeyGitlabBaseUrl] == gi.conf[confKeyGitlabBaseUrl] &&
			excerpt.CreateMetadata[metaKeyGitlabProject] == gi.conf[confKeyProjectID]
	})
	if err == nil {
		return b, nil
	}
	if err != bug.ErrBugNotExist {
		return nil, err
	}

	// if bug was never imported, create bug
	b, _, err = repo.NewBugRaw(
		author,
		issue.CreatedAt.Unix(),
		text.CleanupOneLine(issue.Title),
		text.Cleanup(issue.Description),
		nil,
		map[string]string{
			core.MetaKeyOrigin:   target,
			metaKeyGitlabId:      fmt.Sprintf("%d", issue.IID),
			metaKeyGitlabUrl:     issue.WebURL,
			metaKeyGitlabProject: gi.conf[confKeyProjectID],
			metaKeyGitlabBaseUrl: gi.conf[confKeyGitlabBaseUrl],
		},
	)

	if err != nil {
		return nil, err
	}

	// importing a new bug
	gi.out <- core.NewImportBug(b.Id())

	return b, nil
}

func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCache, issue *gitlab.Issue, event Event) error {
	id, errResolve := b.ResolveOperationWithMetadata(metaKeyGitlabId, event.ID())
	if errResolve != nil && errResolve != cache.ErrNoMatchingOp {
		return errResolve
	}

	// ensure issue author
	author, err := gi.ensurePerson(repo, event.UserID())
	if err != nil {
		return err
	}

	switch event.Kind() {
	case EventClosed:
		if errResolve == nil {
			return nil
		}

		op, err := b.CloseRaw(
			author,
			event.CreatedAt().Unix(),
			map[string]string{
				metaKeyGitlabId: event.ID(),
			},
		)

		if err != nil {
			return err
		}

		gi.out <- core.NewImportStatusChange(op.Id())

	case EventReopened:
		if errResolve == nil {
			return nil
		}

		op, err := b.OpenRaw(
			author,
			event.CreatedAt().Unix(),
			map[string]string{
				metaKeyGitlabId: event.ID(),
			},
		)
		if err != nil {
			return err
		}

		gi.out <- core.NewImportStatusChange(op.Id())

	case EventDescriptionChanged:
		firstComment := b.Snapshot().Comments[0]
		// since gitlab doesn't provide the issue history
		// we should check for "changed the description" notes and compare issue texts
		// TODO: Check only one time and ignore next 'description change' within one issue
		cleanedDesc := text.Cleanup(issue.Description)
		if errResolve == cache.ErrNoMatchingOp && cleanedDesc != firstComment.Message {
			// comment edition
			op, err := b.EditCommentRaw(
				author,
				event.(NoteEvent).UpdatedAt.Unix(),
				firstComment.CombinedId(),
				cleanedDesc,
				map[string]string{
					metaKeyGitlabId: event.ID(),
				},
			)
			if err != nil {
				return err
			}

			gi.out <- core.NewImportTitleEdition(op.Id())
		}

	case EventComment:
		cleanText := text.Cleanup(event.(NoteEvent).Body)

		// if we didn't import the comment
		if errResolve == cache.ErrNoMatchingOp {

			// add comment operation
			op, err := b.AddCommentRaw(
				author,
				event.CreatedAt().Unix(),
				cleanText,
				nil,
				map[string]string{
					metaKeyGitlabId: event.ID(),
				},
			)
			if err != nil {
				return err
			}
			gi.out <- core.NewImportComment(op.Id())
			return nil
		}

		// if comment was already exported

		// search for last comment update
		comment, err := b.Snapshot().SearchCommentByOpId(id)
		if err != nil {
			return err
		}

		// compare local bug comment with the new event body
		if comment.Message != cleanText {
			// comment edition
			op, err := b.EditCommentRaw(
				author,
				event.(NoteEvent).UpdatedAt.Unix(),
				comment.CombinedId(),
				cleanText,
				nil,
			)

			if err != nil {
				return err
			}
			gi.out <- core.NewImportCommentEdition(op.Id())
		}

		return nil

	case EventTitleChanged:
		// title change events are given new notes
		if errResolve == nil {
			return nil
		}

		op, err := b.SetTitleRaw(
			author,
			event.CreatedAt().Unix(),
			event.(NoteEvent).Title(),
			map[string]string{
				metaKeyGitlabId: event.ID(),
			},
		)
		if err != nil {
			return err
		}

		gi.out <- core.NewImportTitleEdition(op.Id())

	case EventAddLabel:
		_, err = b.ForceChangeLabelsRaw(
			author,
			event.CreatedAt().Unix(),
			[]string{event.(LabelEvent).Label.Name},
			nil,
			map[string]string{
				metaKeyGitlabId: event.ID(),
			},
		)
		return err

	case EventRemoveLabel:
		_, err = b.ForceChangeLabelsRaw(
			author,
			event.CreatedAt().Unix(),
			nil,
			[]string{event.(LabelEvent).Label.Name},
			map[string]string{
				metaKeyGitlabId: event.ID(),
			},
		)
		return err

	case EventAssigned,
		EventUnassigned,
		EventChangedMilestone,
		EventRemovedMilestone,
		EventChangedDuedate,
		EventRemovedDuedate,
		EventLocked,
		EventUnlocked,
		EventMentionedInIssue,
		EventMentionedInMergeRequest:

		return nil

	default:
		return fmt.Errorf("unexpected event")
	}

	return nil
}

func (gi *gitlabImporter) ensurePerson(repo *cache.RepoCache, id int) (*cache.IdentityCache, error) {
	// Look first in the cache
	i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGitlabId, strconv.Itoa(id))
	if err == nil {
		return i, nil
	}
	if entity.IsErrMultipleMatch(err) {
		return nil, err
	}

	user, _, err := gi.client.Users.GetUser(id, gitlab.GetUsersOptions{})
	if err != nil {
		return nil, err
	}

	i, err = repo.NewIdentityRaw(
		user.Name,
		user.PublicEmail,
		user.Username,
		user.AvatarURL,
		nil,
		map[string]string{
			// because Gitlab
			metaKeyGitlabId:    strconv.Itoa(id),
			metaKeyGitlabLogin: user.Username,
		},
	)
	if err != nil {
		return nil, err
	}

	gi.out <- core.NewImportIdentity(i.Id())
	return i, nil
}