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 package org.eclipse.jgit.internal.ketch;
45
46 import static org.eclipse.jgit.internal.ketch.Proposal.State.RUNNING;
47
48 import java.io.IOException;
49 import java.util.Collections;
50 import java.util.HashMap;
51 import java.util.HashSet;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Set;
55
56 import org.eclipse.jgit.annotations.Nullable;
57 import org.eclipse.jgit.internal.storage.reftree.Command;
58 import org.eclipse.jgit.internal.storage.reftree.RefTree;
59 import org.eclipse.jgit.lib.CommitBuilder;
60 import org.eclipse.jgit.lib.ObjectId;
61 import org.eclipse.jgit.lib.ObjectInserter;
62 import org.eclipse.jgit.lib.PersonIdent;
63 import org.eclipse.jgit.lib.Ref;
64 import org.eclipse.jgit.lib.Repository;
65 import org.eclipse.jgit.revwalk.RevCommit;
66 import org.eclipse.jgit.revwalk.RevWalk;
67 import org.eclipse.jgit.transport.ReceiveCommand;
68
69
70 class ProposalRound extends Round {
71 private final List<Proposal> todo;
72 private RefTree queuedTree;
73
74 ProposalRound(KetchLeader leader, LogIndex head, List<Proposal> todo,
75 @Nullable RefTree tree) {
76 super(leader, head);
77 this.todo = todo;
78
79 if (tree != null && canCombine(todo)) {
80 this.queuedTree = tree;
81 } else {
82 leader.roundHoldsReferenceToRefTree = false;
83 }
84 }
85
86 private static boolean canCombine(List<Proposal> todo) {
87 Proposal first = todo.get(0);
88 for (int i = 1; i < todo.size(); i++) {
89 if (!canCombine(first, todo.get(i))) {
90 return false;
91 }
92 }
93 return true;
94 }
95
96 private static boolean canCombine(Proposal a, Proposal b) {
97 String aMsg = nullToEmpty(a.getMessage());
98 String bMsg = nullToEmpty(b.getMessage());
99 return aMsg.equals(bMsg) && canCombine(a.getAuthor(), b.getAuthor());
100 }
101
102 private static String nullToEmpty(@Nullable String str) {
103 return str != null ? str : "";
104 }
105
106 private static boolean canCombine(@Nullable PersonIdent a,
107 @Nullable PersonIdent b) {
108 if (a != null && b != null) {
109
110
111
112 return a.getName().equals(b.getName())
113 && a.getEmailAddress().equals(b.getEmailAddress());
114 }
115
116
117 return a == null && b == null;
118 }
119
120 void start() throws IOException {
121 for (Proposal p : todo) {
122 p.notifyState(RUNNING);
123 }
124 try {
125 ObjectId id;
126 try (Repository git = leader.openRepository()) {
127 id = insertProposals(git);
128 }
129 runAsync(id);
130 } catch (NoOp e) {
131 for (Proposal p : todo) {
132 p.success();
133 }
134 leader.lock.lock();
135 try {
136 leader.nextRound();
137 } finally {
138 leader.lock.unlock();
139 }
140 } catch (IOException e) {
141 abort();
142 throw e;
143 }
144 }
145
146 private ObjectId insertProposals(Repository git)
147 throws IOException, NoOp {
148 ObjectId id;
149 try (ObjectInserter inserter = git.newObjectInserter()) {
150
151
152 if (queuedTree != null) {
153 id = insertSingleProposal(git, inserter);
154 } else {
155 id = insertMultiProposal(git, inserter);
156 }
157
158 stageCommands = makeStageList(git, inserter);
159 inserter.flush();
160 }
161 return id;
162 }
163
164 private ObjectId insertSingleProposal(Repository git,
165 ObjectInserter inserter) throws IOException, NoOp {
166
167 ObjectId treeId = queuedTree.writeTree(inserter);
168 queuedTree = null;
169 leader.roundHoldsReferenceToRefTree = false;
170
171 if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
172 try (RevWalk rw = new RevWalk(git)) {
173 RevCommit c = rw.parseCommit(acceptedOldIndex);
174 if (treeId.equals(c.getTree())) {
175 throw new NoOp();
176 }
177 }
178 }
179
180 Proposal p = todo.get(0);
181 CommitBuilder b = new CommitBuilder();
182 b.setTreeId(treeId);
183 if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
184 b.setParentId(acceptedOldIndex);
185 }
186 b.setCommitter(leader.getSystem().newCommitter());
187 b.setAuthor(p.getAuthor() != null ? p.getAuthor() : b.getCommitter());
188 b.setMessage(message(p));
189 return inserter.insert(b);
190 }
191
192 private ObjectId insertMultiProposal(Repository git,
193 ObjectInserter inserter) throws IOException, NoOp {
194
195
196
197 ObjectId lastIndex = acceptedOldIndex;
198 ObjectId oldTreeId;
199 RefTree tree;
200 if (ObjectId.zeroId().equals(lastIndex)) {
201 oldTreeId = ObjectId.zeroId();
202 tree = RefTree.newEmptyTree();
203 } else {
204 try (RevWalk rw = new RevWalk(git)) {
205 RevCommit c = rw.parseCommit(lastIndex);
206 oldTreeId = c.getTree();
207 tree = RefTree.read(rw.getObjectReader(), c.getTree());
208 }
209 }
210
211 PersonIdent committer = leader.getSystem().newCommitter();
212 for (Proposal p : todo) {
213 if (!tree.apply(p.getCommands())) {
214
215
216
217 throw new IOException(
218 KetchText.get().queuedProposalFailedToApply);
219 }
220
221 ObjectId treeId = tree.writeTree(inserter);
222 if (treeId.equals(oldTreeId)) {
223 continue;
224 }
225
226 CommitBuilder b = new CommitBuilder();
227 b.setTreeId(treeId);
228 if (!ObjectId.zeroId().equals(lastIndex)) {
229 b.setParentId(lastIndex);
230 }
231 b.setAuthor(p.getAuthor() != null ? p.getAuthor() : committer);
232 b.setCommitter(committer);
233 b.setMessage(message(p));
234 lastIndex = inserter.insert(b);
235 }
236 if (lastIndex.equals(acceptedOldIndex)) {
237 throw new NoOp();
238 }
239 return lastIndex;
240 }
241
242 private String message(Proposal p) {
243 StringBuilder m = new StringBuilder();
244 String msg = p.getMessage();
245 if (msg != null && !msg.isEmpty()) {
246 m.append(msg);
247 while (m.length() < 2 || m.charAt(m.length() - 2) != '\n'
248 || m.charAt(m.length() - 1) != '\n') {
249 m.append('\n');
250 }
251 }
252 m.append(KetchConstants.TERM.getName())
253 .append(": ")
254 .append(leader.getTerm());
255 return m.toString();
256 }
257
258 void abort() {
259 for (Proposal p : todo) {
260 p.abort();
261 }
262 }
263
264 void success() {
265 for (Proposal p : todo) {
266 p.success();
267 }
268 }
269
270 private List<ReceiveCommand> makeStageList(Repository git,
271 ObjectInserter inserter) throws IOException {
272
273
274
275 Map<String, ObjectId> byRef = new HashMap<>();
276 for (Proposal p : todo) {
277 for (Command c : p.getCommands()) {
278 Ref n = c.getNewRef();
279 if (n != null && !n.isSymbolic()) {
280 byRef.put(n.getName(), n.getObjectId());
281 }
282 }
283 }
284 if (byRef.isEmpty()) {
285 return Collections.emptyList();
286 }
287
288 Set<ObjectId> newObjs = new HashSet<>(byRef.values());
289 StageBuilder b = new StageBuilder(
290 leader.getSystem().getTxnStage(),
291 acceptedNewIndex);
292 return b.makeStageList(newObjs, git, inserter);
293 }
294
295
296 private static class NoOp extends Exception {
297 private static final long serialVersionUID = 1L;
298 }
299 }