code.gitea.io/gitea@v1.21.7/services/migrations/codebase.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package migrations
     5  
     6  import (
     7  	"context"
     8  	"encoding/xml"
     9  	"fmt"
    10  	"net/http"
    11  	"net/url"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"code.gitea.io/gitea/modules/log"
    17  	base "code.gitea.io/gitea/modules/migration"
    18  	"code.gitea.io/gitea/modules/proxy"
    19  	"code.gitea.io/gitea/modules/structs"
    20  )
    21  
    22  var (
    23  	_ base.Downloader        = &CodebaseDownloader{}
    24  	_ base.DownloaderFactory = &CodebaseDownloaderFactory{}
    25  )
    26  
    27  func init() {
    28  	RegisterDownloaderFactory(&CodebaseDownloaderFactory{})
    29  }
    30  
    31  // CodebaseDownloaderFactory defines a downloader factory
    32  type CodebaseDownloaderFactory struct{}
    33  
    34  // New returns a downloader related to this factory according MigrateOptions
    35  func (f *CodebaseDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
    36  	u, err := url.Parse(opts.CloneAddr)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  	u.User = nil
    41  
    42  	fields := strings.Split(strings.Trim(u.Path, "/"), "/")
    43  	if len(fields) != 2 {
    44  		return nil, fmt.Errorf("invalid path: %s", u.Path)
    45  	}
    46  	project := fields[0]
    47  	repoName := strings.TrimSuffix(fields[1], ".git")
    48  
    49  	log.Trace("Create Codebase downloader. BaseURL: %v RepoName: %s", u, repoName)
    50  
    51  	return NewCodebaseDownloader(ctx, u, project, repoName, opts.AuthUsername, opts.AuthPassword), nil
    52  }
    53  
    54  // GitServiceType returns the type of git service
    55  func (f *CodebaseDownloaderFactory) GitServiceType() structs.GitServiceType {
    56  	return structs.CodebaseService
    57  }
    58  
    59  type codebaseUser struct {
    60  	ID    int64  `json:"id"`
    61  	Name  string `json:"name"`
    62  	Email string `json:"email"`
    63  }
    64  
    65  // CodebaseDownloader implements a Downloader interface to get repository information
    66  // from Codebase
    67  type CodebaseDownloader struct {
    68  	base.NullDownloader
    69  	ctx           context.Context
    70  	client        *http.Client
    71  	baseURL       *url.URL
    72  	projectURL    *url.URL
    73  	project       string
    74  	repoName      string
    75  	maxIssueIndex int64
    76  	userMap       map[int64]*codebaseUser
    77  	commitMap     map[string]string
    78  }
    79  
    80  // SetContext set context
    81  func (d *CodebaseDownloader) SetContext(ctx context.Context) {
    82  	d.ctx = ctx
    83  }
    84  
    85  // NewCodebaseDownloader creates a new downloader
    86  func NewCodebaseDownloader(ctx context.Context, projectURL *url.URL, project, repoName, username, password string) *CodebaseDownloader {
    87  	baseURL, _ := url.Parse("https://api3.codebasehq.com")
    88  
    89  	downloader := &CodebaseDownloader{
    90  		ctx:        ctx,
    91  		baseURL:    baseURL,
    92  		projectURL: projectURL,
    93  		project:    project,
    94  		repoName:   repoName,
    95  		client: &http.Client{
    96  			Transport: &http.Transport{
    97  				Proxy: func(req *http.Request) (*url.URL, error) {
    98  					if len(username) > 0 && len(password) > 0 {
    99  						req.SetBasicAuth(username, password)
   100  					}
   101  					return proxy.Proxy()(req)
   102  				},
   103  			},
   104  		},
   105  		userMap:   make(map[int64]*codebaseUser),
   106  		commitMap: make(map[string]string),
   107  	}
   108  
   109  	log.Trace("Create Codebase downloader. BaseURL: %s Project: %s RepoName: %s", baseURL, project, repoName)
   110  	return downloader
   111  }
   112  
   113  // String implements Stringer
   114  func (d *CodebaseDownloader) String() string {
   115  	return fmt.Sprintf("migration from codebase server %s %s/%s", d.baseURL, d.project, d.repoName)
   116  }
   117  
   118  func (d *CodebaseDownloader) LogString() string {
   119  	if d == nil {
   120  		return "<CodebaseDownloader nil>"
   121  	}
   122  	return fmt.Sprintf("<CodebaseDownloader %s %s/%s>", d.baseURL, d.project, d.repoName)
   123  }
   124  
   125  // FormatCloneURL add authentication into remote URLs
   126  func (d *CodebaseDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr string) (string, error) {
   127  	return opts.CloneAddr, nil
   128  }
   129  
   130  func (d *CodebaseDownloader) callAPI(endpoint string, parameter map[string]string, result any) error {
   131  	u, err := d.baseURL.Parse(endpoint)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	if parameter != nil {
   137  		query := u.Query()
   138  		for k, v := range parameter {
   139  			query.Set(k, v)
   140  		}
   141  		u.RawQuery = query.Encode()
   142  	}
   143  
   144  	req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	req.Header.Add("Accept", "application/xml")
   149  
   150  	resp, err := d.client.Do(req)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	defer resp.Body.Close()
   155  
   156  	return xml.NewDecoder(resp.Body).Decode(&result)
   157  }
   158  
   159  // GetRepoInfo returns repository information
   160  // https://support.codebasehq.com/kb/projects
   161  func (d *CodebaseDownloader) GetRepoInfo() (*base.Repository, error) {
   162  	var rawRepository struct {
   163  		XMLName     xml.Name `xml:"repository"`
   164  		Name        string   `xml:"name"`
   165  		Description string   `xml:"description"`
   166  		Permalink   string   `xml:"permalink"`
   167  		CloneURL    string   `xml:"clone-url"`
   168  		Source      string   `xml:"source"`
   169  	}
   170  
   171  	err := d.callAPI(
   172  		fmt.Sprintf("/%s/%s", d.project, d.repoName),
   173  		nil,
   174  		&rawRepository,
   175  	)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	return &base.Repository{
   181  		Name:        rawRepository.Name,
   182  		Description: rawRepository.Description,
   183  		CloneURL:    rawRepository.CloneURL,
   184  		OriginalURL: d.projectURL.String(),
   185  	}, nil
   186  }
   187  
   188  // GetMilestones returns milestones
   189  // https://support.codebasehq.com/kb/tickets-and-milestones/milestones
   190  func (d *CodebaseDownloader) GetMilestones() ([]*base.Milestone, error) {
   191  	var rawMilestones struct {
   192  		XMLName            xml.Name `xml:"ticketing-milestone"`
   193  		Type               string   `xml:"type,attr"`
   194  		TicketingMilestone []struct {
   195  			Text string `xml:",chardata"`
   196  			ID   struct {
   197  				Value int64  `xml:",chardata"`
   198  				Type  string `xml:"type,attr"`
   199  			} `xml:"id"`
   200  			Identifier string `xml:"identifier"`
   201  			Name       string `xml:"name"`
   202  			Deadline   struct {
   203  				Value string `xml:",chardata"`
   204  				Type  string `xml:"type,attr"`
   205  			} `xml:"deadline"`
   206  			Description string `xml:"description"`
   207  			Status      string `xml:"status"`
   208  		} `xml:"ticketing-milestone"`
   209  	}
   210  
   211  	err := d.callAPI(
   212  		fmt.Sprintf("/%s/milestones", d.project),
   213  		nil,
   214  		&rawMilestones,
   215  	)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	milestones := make([]*base.Milestone, 0, len(rawMilestones.TicketingMilestone))
   221  	for _, milestone := range rawMilestones.TicketingMilestone {
   222  		var deadline *time.Time
   223  		if len(milestone.Deadline.Value) > 0 {
   224  			if val, err := time.Parse("2006-01-02", milestone.Deadline.Value); err == nil {
   225  				deadline = &val
   226  			}
   227  		}
   228  
   229  		closed := deadline
   230  		state := "closed"
   231  		if milestone.Status == "active" {
   232  			closed = nil
   233  			state = ""
   234  		}
   235  
   236  		milestones = append(milestones, &base.Milestone{
   237  			Title:    milestone.Name,
   238  			Deadline: deadline,
   239  			Closed:   closed,
   240  			State:    state,
   241  		})
   242  	}
   243  	return milestones, nil
   244  }
   245  
   246  // GetLabels returns labels
   247  // https://support.codebasehq.com/kb/tickets-and-milestones/statuses-priorities-and-categories
   248  func (d *CodebaseDownloader) GetLabels() ([]*base.Label, error) {
   249  	var rawTypes struct {
   250  		XMLName       xml.Name `xml:"ticketing-types"`
   251  		Type          string   `xml:"type,attr"`
   252  		TicketingType []struct {
   253  			ID struct {
   254  				Value int64  `xml:",chardata"`
   255  				Type  string `xml:"type,attr"`
   256  			} `xml:"id"`
   257  			Name string `xml:"name"`
   258  		} `xml:"ticketing-type"`
   259  	}
   260  
   261  	err := d.callAPI(
   262  		fmt.Sprintf("/%s/tickets/types", d.project),
   263  		nil,
   264  		&rawTypes,
   265  	)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  
   270  	labels := make([]*base.Label, 0, len(rawTypes.TicketingType))
   271  	for _, label := range rawTypes.TicketingType {
   272  		labels = append(labels, &base.Label{
   273  			Name:  label.Name,
   274  			Color: "ffffff",
   275  		})
   276  	}
   277  	return labels, nil
   278  }
   279  
   280  type codebaseIssueContext struct {
   281  	Comments []*base.Comment
   282  }
   283  
   284  // GetIssues returns issues, limits are not supported
   285  // https://support.codebasehq.com/kb/tickets-and-milestones
   286  // https://support.codebasehq.com/kb/tickets-and-milestones/updating-tickets
   287  func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
   288  	var rawIssues struct {
   289  		XMLName xml.Name `xml:"tickets"`
   290  		Type    string   `xml:"type,attr"`
   291  		Ticket  []struct {
   292  			TicketID struct {
   293  				Value int64  `xml:",chardata"`
   294  				Type  string `xml:"type,attr"`
   295  			} `xml:"ticket-id"`
   296  			Summary    string `xml:"summary"`
   297  			TicketType string `xml:"ticket-type"`
   298  			ReporterID struct {
   299  				Value int64  `xml:",chardata"`
   300  				Type  string `xml:"type,attr"`
   301  			} `xml:"reporter-id"`
   302  			Reporter string `xml:"reporter"`
   303  			Type     struct {
   304  				Name string `xml:"name"`
   305  			} `xml:"type"`
   306  			Status struct {
   307  				TreatAsClosed struct {
   308  					Value bool   `xml:",chardata"`
   309  					Type  string `xml:"type,attr"`
   310  				} `xml:"treat-as-closed"`
   311  			} `xml:"status"`
   312  			Milestone struct {
   313  				Name string `xml:"name"`
   314  			} `xml:"milestone"`
   315  			UpdatedAt struct {
   316  				Value time.Time `xml:",chardata"`
   317  				Type  string    `xml:"type,attr"`
   318  			} `xml:"updated-at"`
   319  			CreatedAt struct {
   320  				Value time.Time `xml:",chardata"`
   321  				Type  string    `xml:"type,attr"`
   322  			} `xml:"created-at"`
   323  		} `xml:"ticket"`
   324  	}
   325  
   326  	err := d.callAPI(
   327  		fmt.Sprintf("/%s/tickets", d.project),
   328  		nil,
   329  		&rawIssues,
   330  	)
   331  	if err != nil {
   332  		return nil, false, err
   333  	}
   334  
   335  	issues := make([]*base.Issue, 0, len(rawIssues.Ticket))
   336  	for _, issue := range rawIssues.Ticket {
   337  		var notes struct {
   338  			XMLName    xml.Name `xml:"ticket-notes"`
   339  			Type       string   `xml:"type,attr"`
   340  			TicketNote []struct {
   341  				Content   string `xml:"content"`
   342  				CreatedAt struct {
   343  					Value time.Time `xml:",chardata"`
   344  					Type  string    `xml:"type,attr"`
   345  				} `xml:"created-at"`
   346  				UpdatedAt struct {
   347  					Value time.Time `xml:",chardata"`
   348  					Type  string    `xml:"type,attr"`
   349  				} `xml:"updated-at"`
   350  				ID struct {
   351  					Value int64  `xml:",chardata"`
   352  					Type  string `xml:"type,attr"`
   353  				} `xml:"id"`
   354  				UserID struct {
   355  					Value int64  `xml:",chardata"`
   356  					Type  string `xml:"type,attr"`
   357  				} `xml:"user-id"`
   358  			} `xml:"ticket-note"`
   359  		}
   360  		err := d.callAPI(
   361  			fmt.Sprintf("/%s/tickets/%d/notes", d.project, issue.TicketID.Value),
   362  			nil,
   363  			&notes,
   364  		)
   365  		if err != nil {
   366  			return nil, false, err
   367  		}
   368  		comments := make([]*base.Comment, 0, len(notes.TicketNote))
   369  		for _, note := range notes.TicketNote {
   370  			if len(note.Content) == 0 {
   371  				continue
   372  			}
   373  			poster := d.tryGetUser(note.UserID.Value)
   374  			comments = append(comments, &base.Comment{
   375  				IssueIndex:  issue.TicketID.Value,
   376  				Index:       note.ID.Value,
   377  				PosterID:    poster.ID,
   378  				PosterName:  poster.Name,
   379  				PosterEmail: poster.Email,
   380  				Content:     note.Content,
   381  				Created:     note.CreatedAt.Value,
   382  				Updated:     note.UpdatedAt.Value,
   383  			})
   384  		}
   385  		if len(comments) == 0 {
   386  			comments = append(comments, &base.Comment{})
   387  		}
   388  
   389  		state := "open"
   390  		if issue.Status.TreatAsClosed.Value {
   391  			state = "closed"
   392  		}
   393  		poster := d.tryGetUser(issue.ReporterID.Value)
   394  		issues = append(issues, &base.Issue{
   395  			Title:       issue.Summary,
   396  			Number:      issue.TicketID.Value,
   397  			PosterName:  poster.Name,
   398  			PosterEmail: poster.Email,
   399  			Content:     comments[0].Content,
   400  			Milestone:   issue.Milestone.Name,
   401  			State:       state,
   402  			Created:     issue.CreatedAt.Value,
   403  			Updated:     issue.UpdatedAt.Value,
   404  			Labels: []*base.Label{
   405  				{Name: issue.Type.Name},
   406  			},
   407  			ForeignIndex: issue.TicketID.Value,
   408  			Context: codebaseIssueContext{
   409  				Comments: comments[1:],
   410  			},
   411  		})
   412  
   413  		if d.maxIssueIndex < issue.TicketID.Value {
   414  			d.maxIssueIndex = issue.TicketID.Value
   415  		}
   416  	}
   417  
   418  	return issues, true, nil
   419  }
   420  
   421  // GetComments returns comments
   422  func (d *CodebaseDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
   423  	context, ok := commentable.GetContext().(codebaseIssueContext)
   424  	if !ok {
   425  		return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
   426  	}
   427  
   428  	return context.Comments, true, nil
   429  }
   430  
   431  // GetPullRequests returns pull requests
   432  // https://support.codebasehq.com/kb/repositories/merge-requests
   433  func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
   434  	var rawMergeRequests struct {
   435  		XMLName      xml.Name `xml:"merge-requests"`
   436  		Type         string   `xml:"type,attr"`
   437  		MergeRequest []struct {
   438  			ID struct {
   439  				Value int64  `xml:",chardata"`
   440  				Type  string `xml:"type,attr"`
   441  			} `xml:"id"`
   442  		} `xml:"merge-request"`
   443  	}
   444  
   445  	err := d.callAPI(
   446  		fmt.Sprintf("/%s/%s/merge_requests", d.project, d.repoName),
   447  		map[string]string{
   448  			"query":  `"Target Project" is "` + d.repoName + `"`,
   449  			"offset": strconv.Itoa((page - 1) * perPage),
   450  			"count":  strconv.Itoa(perPage),
   451  		},
   452  		&rawMergeRequests,
   453  	)
   454  	if err != nil {
   455  		return nil, false, err
   456  	}
   457  
   458  	pullRequests := make([]*base.PullRequest, 0, len(rawMergeRequests.MergeRequest))
   459  	for i, mr := range rawMergeRequests.MergeRequest {
   460  		var rawMergeRequest struct {
   461  			XMLName xml.Name `xml:"merge-request"`
   462  			ID      struct {
   463  				Value int64  `xml:",chardata"`
   464  				Type  string `xml:"type,attr"`
   465  			} `xml:"id"`
   466  			SourceRef string `xml:"source-ref"` // NOTE: from the documentation these are actually just branches NOT full refs
   467  			TargetRef string `xml:"target-ref"` // NOTE: from the documentation these are actually just branches NOT full refs
   468  			Subject   string `xml:"subject"`
   469  			Status    string `xml:"status"`
   470  			UserID    struct {
   471  				Value int64  `xml:",chardata"`
   472  				Type  string `xml:"type,attr"`
   473  			} `xml:"user-id"`
   474  			CreatedAt struct {
   475  				Value time.Time `xml:",chardata"`
   476  				Type  string    `xml:"type,attr"`
   477  			} `xml:"created-at"`
   478  			UpdatedAt struct {
   479  				Value time.Time `xml:",chardata"`
   480  				Type  string    `xml:"type,attr"`
   481  			} `xml:"updated-at"`
   482  			Comments struct {
   483  				Type    string `xml:"type,attr"`
   484  				Comment []struct {
   485  					Content string `xml:"content"`
   486  					ID      struct {
   487  						Value int64  `xml:",chardata"`
   488  						Type  string `xml:"type,attr"`
   489  					} `xml:"id"`
   490  					UserID struct {
   491  						Value int64  `xml:",chardata"`
   492  						Type  string `xml:"type,attr"`
   493  					} `xml:"user-id"`
   494  					Action struct {
   495  						Value string `xml:",chardata"`
   496  						Nil   string `xml:"nil,attr"`
   497  					} `xml:"action"`
   498  					CreatedAt struct {
   499  						Value time.Time `xml:",chardata"`
   500  						Type  string    `xml:"type,attr"`
   501  					} `xml:"created-at"`
   502  				} `xml:"comment"`
   503  			} `xml:"comments"`
   504  		}
   505  		err := d.callAPI(
   506  			fmt.Sprintf("/%s/%s/merge_requests/%d", d.project, d.repoName, mr.ID.Value),
   507  			nil,
   508  			&rawMergeRequest,
   509  		)
   510  		if err != nil {
   511  			return nil, false, err
   512  		}
   513  
   514  		number := d.maxIssueIndex + int64(i) + 1
   515  
   516  		state := "open"
   517  		merged := false
   518  		var closeTime *time.Time
   519  		var mergedTime *time.Time
   520  		if rawMergeRequest.Status != "new" {
   521  			state = "closed"
   522  			closeTime = &rawMergeRequest.UpdatedAt.Value
   523  		}
   524  
   525  		comments := make([]*base.Comment, 0, len(rawMergeRequest.Comments.Comment))
   526  		for _, comment := range rawMergeRequest.Comments.Comment {
   527  			if len(comment.Content) == 0 {
   528  				if comment.Action.Value == "merging" {
   529  					merged = true
   530  					mergedTime = &comment.CreatedAt.Value
   531  				}
   532  				continue
   533  			}
   534  			poster := d.tryGetUser(comment.UserID.Value)
   535  			comments = append(comments, &base.Comment{
   536  				IssueIndex:  number,
   537  				Index:       comment.ID.Value,
   538  				PosterID:    poster.ID,
   539  				PosterName:  poster.Name,
   540  				PosterEmail: poster.Email,
   541  				Content:     comment.Content,
   542  				Created:     comment.CreatedAt.Value,
   543  				Updated:     comment.CreatedAt.Value,
   544  			})
   545  		}
   546  		if len(comments) == 0 {
   547  			comments = append(comments, &base.Comment{})
   548  		}
   549  
   550  		poster := d.tryGetUser(rawMergeRequest.UserID.Value)
   551  
   552  		pullRequests = append(pullRequests, &base.PullRequest{
   553  			Title:       rawMergeRequest.Subject,
   554  			Number:      number,
   555  			PosterName:  poster.Name,
   556  			PosterEmail: poster.Email,
   557  			Content:     comments[0].Content,
   558  			State:       state,
   559  			Created:     rawMergeRequest.CreatedAt.Value,
   560  			Updated:     rawMergeRequest.UpdatedAt.Value,
   561  			Closed:      closeTime,
   562  			Merged:      merged,
   563  			MergedTime:  mergedTime,
   564  			Head: base.PullRequestBranch{
   565  				Ref:      rawMergeRequest.SourceRef,
   566  				SHA:      d.getHeadCommit(rawMergeRequest.SourceRef),
   567  				RepoName: d.repoName,
   568  			},
   569  			Base: base.PullRequestBranch{
   570  				Ref:      rawMergeRequest.TargetRef,
   571  				SHA:      d.getHeadCommit(rawMergeRequest.TargetRef),
   572  				RepoName: d.repoName,
   573  			},
   574  			ForeignIndex: rawMergeRequest.ID.Value,
   575  			Context: codebaseIssueContext{
   576  				Comments: comments[1:],
   577  			},
   578  		})
   579  
   580  		// SECURITY: Ensure that the PR is safe
   581  		_ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], d.baseURL.String(), d)
   582  	}
   583  
   584  	return pullRequests, true, nil
   585  }
   586  
   587  func (d *CodebaseDownloader) tryGetUser(userID int64) *codebaseUser {
   588  	if len(d.userMap) == 0 {
   589  		var rawUsers struct {
   590  			XMLName xml.Name `xml:"users"`
   591  			Type    string   `xml:"type,attr"`
   592  			User    []struct {
   593  				EmailAddress string `xml:"email-address"`
   594  				ID           struct {
   595  					Value int64  `xml:",chardata"`
   596  					Type  string `xml:"type,attr"`
   597  				} `xml:"id"`
   598  				LastName  string `xml:"last-name"`
   599  				FirstName string `xml:"first-name"`
   600  				Username  string `xml:"username"`
   601  			} `xml:"user"`
   602  		}
   603  
   604  		err := d.callAPI(
   605  			"/users",
   606  			nil,
   607  			&rawUsers,
   608  		)
   609  		if err == nil {
   610  			for _, user := range rawUsers.User {
   611  				d.userMap[user.ID.Value] = &codebaseUser{
   612  					Name:  user.Username,
   613  					Email: user.EmailAddress,
   614  				}
   615  			}
   616  		}
   617  	}
   618  
   619  	user, ok := d.userMap[userID]
   620  	if !ok {
   621  		user = &codebaseUser{
   622  			Name: fmt.Sprintf("User %d", userID),
   623  		}
   624  		d.userMap[userID] = user
   625  	}
   626  
   627  	return user
   628  }
   629  
   630  func (d *CodebaseDownloader) getHeadCommit(ref string) string {
   631  	commitRef, ok := d.commitMap[ref]
   632  	if !ok {
   633  		var rawCommits struct {
   634  			XMLName xml.Name `xml:"commits"`
   635  			Type    string   `xml:"type,attr"`
   636  			Commit  []struct {
   637  				Ref string `xml:"ref"`
   638  			} `xml:"commit"`
   639  		}
   640  		err := d.callAPI(
   641  			fmt.Sprintf("/%s/%s/commits/%s", d.project, d.repoName, ref),
   642  			nil,
   643  			&rawCommits,
   644  		)
   645  		if err == nil && len(rawCommits.Commit) > 0 {
   646  			commitRef = rawCommits.Commit[0].Ref
   647  			d.commitMap[ref] = commitRef
   648  		}
   649  	}
   650  	return commitRef
   651  }