about summary refs log tree commit diff
path: root/submitqueue/submitqueue.go
blob: bd0ee230a903e7eed27da8eb053359b73ba9bdd4 (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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
package submitqueue

import (
	"fmt"

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

	"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
	URL            string
}

// 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 changesets, and assembling them to Series.
func (s *SubmitQueue) LoadSeries(log *logrus.Logger) error {
	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
	}

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

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

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

// IsAutoSubmittable returns true if a given Serie has all the necessary flags set
// meaning it would be fine to rebase and/or submit it.
// This means, every changeset needs to:
// - have the s.SubmitQueueTag hashtag
// - be verified (+1 by CI)
// - be code reviewed (+2 by a human)
func (s *SubmitQueue) IsAutoSubmittable(serie *Serie) bool {
	return serie.FilterAllChangesets(func(c *gerrit.Changeset) bool {
		return c.HasTag(s.SubmitQueueTag) && c.IsVerified && c.IsCodeReviewed
	})
}

// GetChangesetURL returns the URL to view a given changeset
func (s *SubmitQueue) GetChangesetURL(changeset *gerrit.Changeset) string {
	return fmt.Sprintf("%s/c/%s/+/%d", s.gerrit.GetBaseURL(), s.ProjectName, changeset.Number)
}

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

	// TODO: actually log more!

	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, skipping", serie.String())
		}

		// if serie is auto-submittable and rebased on top of current master…
		if s.IsAutoSubmittable(serie) && 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 the next auto-submittable series on top of current HEAD
// they are still ordered by series size
// After a DoRebase, consumers are supposed to fetch state again via LoadSeries,
// as things most likely have changed, and error handling during partially failed rebases
// is really tricky
func (s *SubmitQueue) DoRebase(log *logrus.Logger) error {
	if s.HEAD == "" {
		return fmt.Errorf("current HEAD is an empty string, bailing out")
	}
	for _, serie := range s.Series {
		logger := log.WithFields(logrus.Fields{
			"serie": serie,
		})
		if !s.IsAutoSubmittable(serie) {
			logger.Debug("skipping non-auto-submittable series")
			continue
		}

		logger.Infof("rebasing on top of %s", s.HEAD)
		_, err := s.RebaseSerie(serie, s.HEAD)
		if err != nil {
			// We skip trivial rebase errors instead of bailing out.
			// TODO: we might want to remove s.SubmitQueueTag from the changeset,
			// but even without doing it,
			// we're merly spanning, and won't get stuck in trying to rebase the same
			// changeset over and over again, as some other changeset will likely succeed
			// with rebasing and will be merged by DoSubmit.
			logger.Warnf("failure while rebasing, continuing with next one: %s", err)
			continue
		} else {
			logger.Info("success rebasing on top of %s", s.HEAD)
			break
		}
	}

	return nil
}

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

	log := logrus.New()
	log.AddHook(r)

	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)
		r.Error = err
		return r
	}
	s.HEAD = commitID

	err = s.LoadSeries(log)
	if err != nil {
		r.Error = err
		return r
	}

	// copy series to result object
	for _, serie := range s.Series {
		r.Series = append(r.Series, *serie)
	}

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

// 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
}