about summary refs log tree commit diff
path: root/submitqueue/submitqueue.go
blob: b7c0993fb6f10224738226047637e4edcfbaba24 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
package submitqueue

import (
	"fmt"

	"github.com/tweag/gerrit-queue/gerrit"

	log "github.com/sirupsen/logrus"
)

// SubmitQueue contains a list of series, a gerrit connection, and some project configuration
type SubmitQueue struct {
	Series         []*Serie
	gerrit         gerrit.IClient
	ProjectName    string
	BranchName     string
	HEAD           string
	SubmitQueueTag string // the tag used to submit something to the submit queue
}

// MakeSubmitQueue builds a new submit queue
func MakeSubmitQueue(gerritClient gerrit.IClient, projectName string, branchName string, submitQueueTag string) SubmitQueue {
	return SubmitQueue{
		Series:         make([]*Serie, 0),
		gerrit:         gerritClient,
		ProjectName:    projectName,
		BranchName:     branchName,
		SubmitQueueTag: submitQueueTag,
	}
}

// LoadSeries fills Series by searching and filtering changesets, and assembling them to Series.
func (s *SubmitQueue) LoadSeries() error {
	// Normally, we'd like to use a queryString like
	// "status:open project:myproject branch:mybranch hashtag:submitQueueTag label:verified=+1 label:code-review=+2"
	// to avoid filtering client-side
	// Due to https://github.com/andygrunwald/go-gerrit/issues/71,
	// we need to do this on the client (filterChangesets)
	var queryString = fmt.Sprintf("status:open project:%s branch:%s", s.ProjectName, s.BranchName)
	log.Debugf("Running query %s", queryString)

	// Download changesets from gerrit
	changesets, err := s.gerrit.SearchChangesets(queryString)
	if err != nil {
		return err
	}
	// // Filter to contain the SubmitQueueTag
	// changesets = gerrit.FilterChangesets(changesets, func(c *gerrit.Changeset) bool {
	// 	return c.HasTag(SubmitQueueTag)
	// })
	// Filter to be code reviewed and verified
	changesets = gerrit.FilterChangesets(changesets, func(c *gerrit.Changeset) bool {
		return c.IsCodeReviewed && c.IsVerified
	})

	// Assemble to series
	series, err := AssembleSeries(changesets)
	if err != nil {
		return err
	}

	// Sort by size
	s.Series = SortSeries(series)
	return nil
}

// UpdateHEAD updates the HEAD field with the commit ID of the current HEAD
func (s *SubmitQueue) UpdateHEAD() error {
	HEAD, err := s.gerrit.GetHEAD(s.ProjectName, s.BranchName)
	if err != nil {
		return err
	}
	s.HEAD = HEAD
	return nil
}

// TODO: clear submit queue tag if missing +1/+2?

// DoSubmit submits changes that can be submitted,
// and updates `Series` to contain the remaining ones
// Also updates `BranchCommitID`.
func (s *SubmitQueue) DoSubmit() error {
	var remainingSeries []*Serie

	for _, serie := range s.Series {
		serieParentCommitIDs, err := serie.GetParentCommitIDs()
		if err != nil {
			return err
		}
		// we can only submit series with a single parent commit (otherwise they're not rebased)
		if len(serieParentCommitIDs) != 1 {
			return fmt.Errorf("%s has more than one parent commit", serie.String())
		}
		// if serie is rebased on top of current master…
		if serieParentCommitIDs[0] == s.HEAD {
			// submit the last changeset of the series, which submits intermediate ones too
			_, err := s.gerrit.SubmitChangeset(serie.ChangeSets[len(serie.ChangeSets)-1])
			if err != nil {
				// this might fail, for various reasons:
				//  - developers could have updated the changeset meanwhile, clearing +1/+2 bits
				//  - master might have advanced, so this changeset isn't rebased on top of master
				// TODO: we currently bail out entirely, but should be fine on the
				// next loop. We might later want to improve the logic to be a bit more
				// smarter (like log and try with the next one)
				return err
			}
			// advance head to the leaf of the current serie for the next iteration
			newHead, err := serie.GetLeafCommitID()
			if err != nil {
				return err
			}
			s.HEAD = newHead
		} else {
			remainingSeries = append(remainingSeries, serie)
		}
	}

	s.Series = remainingSeries
	return nil
}

// DoRebase rebases all remaining series on top of each other
// they should still be ordered by series size
// TODO: this will produce a very large series on the next run, so we might want to preserve individual series over multiple runs
func (s *SubmitQueue) DoRebase() error {
	newSeries := make([]*Serie, len(s.Series))
	futureHEAD := s.HEAD
	for _, serie := range s.Series {
		//TODO: don't rebase everything, just pick a "good candidate"

		logger := log.WithFields(log.Fields{
			"serie": serie,
		})
		logger.Infof("rebasing %s on top of %s", serie, futureHEAD)
		newSerie, err := s.RebaseSerie(serie, futureHEAD)
		if err != nil {
			logger.Warnf("unable to rebase serie %s", err)
			// TODO: we want to skip on trivial rebase errors instead of bailing out.
			// skip means adding that serie as it is to newSeries, without advancing previousLeafCommitId

			// TODO: we also should talk about when to remove the submit-queue tag
			// just because we scheduled a conflicting submit plan, doesn't mean this is not submittable.
			// so just removing the submit-queue tag would be unfair
			return err
		}
		newSeries = append(newSeries, newSerie)

		// prepare for next iteration
		futureHEAD, err = newSerie.GetLeafCommitID()
		if err != nil {
			// This should never happen
			logger.Errorf("new serie shouldn't be empty: %s", newSerie)
			return err
		}

	}
	s.Series = newSeries
	return nil
}

// Run starts the submit and rebase logic.
func (s *SubmitQueue) Run() error {
	//TODO: log decisions made and add to some ring buffer
	var err error

	commitID, err := s.gerrit.GetHEAD(s.ProjectName, s.BranchName)
	if err != nil {
		log.Errorf("Unable to retrieve HEAD of branch %s at project %s: %s", s.BranchName, s.ProjectName, err)
		return err
	}
	s.HEAD = commitID

	err = s.LoadSeries()
	if err != nil {
		return err
	}
	if len(s.Series) == 0 {
		// Nothing to do!
		log.Warn("Nothing to do here")
		return nil
	}
	err = s.DoSubmit()
	if err != nil {
		return err
	}
	err = s.DoRebase()
	if err != nil {
		return err
	}
	return nil
}

// RebaseSerie rebases a whole serie on top of a given ref
// TODO: only rebase a single changeset. we don't really want to join disconnected series, by rebasing them on top of each other.
func (s *SubmitQueue) RebaseSerie(serie *Serie, ref string) (*Serie, error) {
	newSeries := &Serie{
		ChangeSets: make([]*gerrit.Changeset, len(serie.ChangeSets)),
	}

	rebaseOnto := ref
	for _, changeset := range serie.ChangeSets {
		newChangeset, err := s.gerrit.RebaseChangeset(changeset, rebaseOnto)

		if err != nil {
			// uh-oh…
			// TODO: think about error handling
			// TODO: remove the submit queue tag if the rebase fails (but only then, not on other errors)
			return newSeries, err
		}
		newSeries.ChangeSets = append(newSeries.ChangeSets, newChangeset)

		// the next changeset should be rebased on top of the current commit
		rebaseOnto = newChangeset.CommitID
	}
	return newSeries, nil
}