commit | author | age
|
5e3521
|
1 |
/*
|
JM |
2 |
* Copyright 2013 gitblit.com.
|
|
3 |
*
|
|
4 |
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5 |
* you may not use this file except in compliance with the License.
|
|
6 |
* You may obtain a copy of the License at
|
|
7 |
*
|
|
8 |
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9 |
*
|
|
10 |
* Unless required by applicable law or agreed to in writing, software
|
|
11 |
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12 |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13 |
* See the License for the specific language governing permissions and
|
|
14 |
* limitations under the License.
|
|
15 |
*/
|
|
16 |
package com.gitblit.git;
|
|
17 |
|
|
18 |
import static org.eclipse.jgit.transport.BasePackPushConnection.CAPABILITY_SIDE_BAND_64K;
|
|
19 |
|
|
20 |
import java.io.IOException;
|
|
21 |
import java.text.MessageFormat;
|
|
22 |
import java.util.ArrayList;
|
|
23 |
import java.util.Arrays;
|
|
24 |
import java.util.Collection;
|
|
25 |
import java.util.LinkedHashMap;
|
|
26 |
import java.util.List;
|
|
27 |
import java.util.Map;
|
|
28 |
import java.util.Set;
|
|
29 |
import java.util.concurrent.TimeUnit;
|
|
30 |
import java.util.regex.Matcher;
|
|
31 |
import java.util.regex.Pattern;
|
|
32 |
|
|
33 |
import org.eclipse.jgit.lib.BatchRefUpdate;
|
|
34 |
import org.eclipse.jgit.lib.NullProgressMonitor;
|
|
35 |
import org.eclipse.jgit.lib.ObjectId;
|
|
36 |
import org.eclipse.jgit.lib.PersonIdent;
|
|
37 |
import org.eclipse.jgit.lib.ProgressMonitor;
|
|
38 |
import org.eclipse.jgit.lib.Ref;
|
|
39 |
import org.eclipse.jgit.lib.RefUpdate;
|
|
40 |
import org.eclipse.jgit.lib.Repository;
|
|
41 |
import org.eclipse.jgit.revwalk.RevCommit;
|
|
42 |
import org.eclipse.jgit.revwalk.RevSort;
|
|
43 |
import org.eclipse.jgit.revwalk.RevWalk;
|
|
44 |
import org.eclipse.jgit.transport.ReceiveCommand;
|
|
45 |
import org.eclipse.jgit.transport.ReceiveCommand.Result;
|
|
46 |
import org.eclipse.jgit.transport.ReceiveCommand.Type;
|
|
47 |
import org.eclipse.jgit.transport.ReceivePack;
|
|
48 |
import org.slf4j.Logger;
|
|
49 |
import org.slf4j.LoggerFactory;
|
|
50 |
|
|
51 |
import com.gitblit.Constants;
|
|
52 |
import com.gitblit.Keys;
|
aa89be
|
53 |
import com.gitblit.extensions.PatchsetHook;
|
5e3521
|
54 |
import com.gitblit.manager.IGitblit;
|
JM |
55 |
import com.gitblit.models.RepositoryModel;
|
|
56 |
import com.gitblit.models.TicketModel;
|
|
57 |
import com.gitblit.models.TicketModel.Change;
|
|
58 |
import com.gitblit.models.TicketModel.Field;
|
|
59 |
import com.gitblit.models.TicketModel.Patchset;
|
|
60 |
import com.gitblit.models.TicketModel.PatchsetType;
|
|
61 |
import com.gitblit.models.TicketModel.Status;
|
c2188a
|
62 |
import com.gitblit.models.TicketModel.TicketAction;
|
PM |
63 |
import com.gitblit.models.TicketModel.TicketLink;
|
5e3521
|
64 |
import com.gitblit.models.UserModel;
|
988334
|
65 |
import com.gitblit.tickets.BranchTicketService;
|
5e3521
|
66 |
import com.gitblit.tickets.ITicketService;
|
JM |
67 |
import com.gitblit.tickets.TicketMilestone;
|
|
68 |
import com.gitblit.tickets.TicketNotifier;
|
|
69 |
import com.gitblit.utils.ArrayUtils;
|
|
70 |
import com.gitblit.utils.DiffUtils;
|
|
71 |
import com.gitblit.utils.DiffUtils.DiffStat;
|
|
72 |
import com.gitblit.utils.JGitUtils;
|
|
73 |
import com.gitblit.utils.JGitUtils.MergeResult;
|
|
74 |
import com.gitblit.utils.JGitUtils.MergeStatus;
|
|
75 |
import com.gitblit.utils.RefLogUtils;
|
|
76 |
import com.gitblit.utils.StringUtils;
|
2e73ef
|
77 |
import com.google.common.collect.Lists;
|
5e3521
|
78 |
|
JM |
79 |
|
|
80 |
/**
|
|
81 |
* PatchsetReceivePack processes receive commands and allows for creating, updating,
|
|
82 |
* and closing Gitblit tickets. It also executes Groovy pre- and post- receive
|
|
83 |
* hooks.
|
|
84 |
*
|
|
85 |
* The patchset mechanism defined in this class is based on the ReceiveCommits class
|
|
86 |
* from the Gerrit code review server.
|
|
87 |
*
|
|
88 |
* The general execution flow is:
|
|
89 |
* <ol>
|
|
90 |
* <li>onPreReceive()</li>
|
|
91 |
* <li>executeCommands()</li>
|
|
92 |
* <li>onPostReceive()</li>
|
|
93 |
* </ol>
|
|
94 |
*
|
|
95 |
* @author Android Open Source Project
|
|
96 |
* @author James Moger
|
|
97 |
*
|
|
98 |
*/
|
|
99 |
public class PatchsetReceivePack extends GitblitReceivePack {
|
|
100 |
|
|
101 |
protected static final List<String> MAGIC_REFS = Arrays.asList(Constants.R_FOR, Constants.R_TICKET);
|
|
102 |
|
|
103 |
protected static final Pattern NEW_PATCHSET =
|
|
104 |
Pattern.compile("^refs/tickets/(?:[0-9a-zA-Z][0-9a-zA-Z]/)?([1-9][0-9]*)(?:/new)?$");
|
|
105 |
|
|
106 |
private static final Logger LOGGER = LoggerFactory.getLogger(PatchsetReceivePack.class);
|
|
107 |
|
|
108 |
protected final ITicketService ticketService;
|
|
109 |
|
|
110 |
protected final TicketNotifier ticketNotifier;
|
|
111 |
|
988334
|
112 |
private boolean requireMergeablePatchset;
|
5e3521
|
113 |
|
JM |
114 |
public PatchsetReceivePack(IGitblit gitblit, Repository db, RepositoryModel repository, UserModel user) {
|
|
115 |
super(gitblit, db, repository, user);
|
|
116 |
this.ticketService = gitblit.getTicketService();
|
|
117 |
this.ticketNotifier = ticketService.createNotifier();
|
|
118 |
}
|
|
119 |
|
|
120 |
/** Returns the patchset ref root from the ref */
|
|
121 |
private String getPatchsetRef(String refName) {
|
|
122 |
for (String patchRef : MAGIC_REFS) {
|
|
123 |
if (refName.startsWith(patchRef)) {
|
|
124 |
return patchRef;
|
|
125 |
}
|
|
126 |
}
|
|
127 |
return null;
|
|
128 |
}
|
|
129 |
|
|
130 |
/** Checks if the supplied ref name is a patchset ref */
|
|
131 |
private boolean isPatchsetRef(String refName) {
|
|
132 |
return !StringUtils.isEmpty(getPatchsetRef(refName));
|
|
133 |
}
|
|
134 |
|
|
135 |
/** Checks if the supplied ref name is a change ref */
|
|
136 |
private boolean isTicketRef(String refName) {
|
|
137 |
return refName.startsWith(Constants.R_TICKETS_PATCHSETS);
|
|
138 |
}
|
|
139 |
|
|
140 |
/** Extracts the integration branch from the ref name */
|
|
141 |
private String getIntegrationBranch(String refName) {
|
|
142 |
String patchsetRef = getPatchsetRef(refName);
|
|
143 |
String branch = refName.substring(patchsetRef.length());
|
|
144 |
if (branch.indexOf('%') > -1) {
|
|
145 |
branch = branch.substring(0, branch.indexOf('%'));
|
|
146 |
}
|
|
147 |
|
|
148 |
String defaultBranch = "master";
|
|
149 |
try {
|
|
150 |
defaultBranch = getRepository().getBranch();
|
|
151 |
} catch (Exception e) {
|
|
152 |
LOGGER.error("failed to determine default branch for " + repository.name, e);
|
|
153 |
}
|
|
154 |
|
600d43
|
155 |
if (!StringUtils.isEmpty(getRepositoryModel().mergeTo)) {
|
JM |
156 |
// repository settings specifies a default integration branch
|
|
157 |
defaultBranch = Repository.shortenRefName(getRepositoryModel().mergeTo);
|
|
158 |
}
|
|
159 |
|
5e3521
|
160 |
long ticketId = 0L;
|
JM |
161 |
try {
|
|
162 |
ticketId = Long.parseLong(branch);
|
|
163 |
} catch (Exception e) {
|
|
164 |
// not a number
|
|
165 |
}
|
|
166 |
if (ticketId > 0 || branch.equalsIgnoreCase("default") || branch.equalsIgnoreCase("new")) {
|
|
167 |
return defaultBranch;
|
|
168 |
}
|
|
169 |
return branch;
|
|
170 |
}
|
|
171 |
|
|
172 |
/** Extracts the ticket id from the ref name */
|
|
173 |
private long getTicketId(String refName) {
|
120794
|
174 |
if (refName.indexOf('%') > -1) {
|
JM |
175 |
refName = refName.substring(0, refName.indexOf('%'));
|
|
176 |
}
|
5e3521
|
177 |
if (refName.startsWith(Constants.R_FOR)) {
|
JM |
178 |
String ref = refName.substring(Constants.R_FOR.length());
|
|
179 |
try {
|
|
180 |
return Long.parseLong(ref);
|
|
181 |
} catch (Exception e) {
|
|
182 |
// not a number
|
|
183 |
}
|
|
184 |
} else if (refName.startsWith(Constants.R_TICKET) ||
|
|
185 |
refName.startsWith(Constants.R_TICKETS_PATCHSETS)) {
|
|
186 |
return PatchsetCommand.getTicketNumber(refName);
|
|
187 |
}
|
|
188 |
return 0L;
|
|
189 |
}
|
|
190 |
|
|
191 |
/** Returns true if the ref namespace exists */
|
|
192 |
private boolean hasRefNamespace(String ref) {
|
|
193 |
Map<String, Ref> blockingFors;
|
|
194 |
try {
|
|
195 |
blockingFors = getRepository().getRefDatabase().getRefs(ref);
|
|
196 |
} catch (IOException err) {
|
|
197 |
sendError("Cannot scan refs in {0}", repository.name);
|
|
198 |
LOGGER.error("Error!", err);
|
|
199 |
return true;
|
|
200 |
}
|
|
201 |
if (!blockingFors.isEmpty()) {
|
|
202 |
sendError("{0} needs the following refs removed to receive patchsets: {1}",
|
|
203 |
repository.name, blockingFors.keySet());
|
|
204 |
return true;
|
|
205 |
}
|
|
206 |
return false;
|
|
207 |
}
|
|
208 |
|
|
209 |
/** Removes change ref receive commands */
|
|
210 |
private List<ReceiveCommand> excludeTicketCommands(Collection<ReceiveCommand> commands) {
|
|
211 |
List<ReceiveCommand> filtered = new ArrayList<ReceiveCommand>();
|
|
212 |
for (ReceiveCommand cmd : commands) {
|
|
213 |
if (!isTicketRef(cmd.getRefName())) {
|
|
214 |
// this is not a ticket ref update
|
|
215 |
filtered.add(cmd);
|
|
216 |
}
|
|
217 |
}
|
|
218 |
return filtered;
|
|
219 |
}
|
|
220 |
|
|
221 |
/** Removes patchset receive commands for pre- and post- hook integrations */
|
|
222 |
private List<ReceiveCommand> excludePatchsetCommands(Collection<ReceiveCommand> commands) {
|
|
223 |
List<ReceiveCommand> filtered = new ArrayList<ReceiveCommand>();
|
|
224 |
for (ReceiveCommand cmd : commands) {
|
|
225 |
if (!isPatchsetRef(cmd.getRefName())) {
|
|
226 |
// this is a non-patchset ref update
|
|
227 |
filtered.add(cmd);
|
|
228 |
}
|
|
229 |
}
|
|
230 |
return filtered;
|
|
231 |
}
|
|
232 |
|
|
233 |
/** Process receive commands EXCEPT for Patchset commands. */
|
|
234 |
@Override
|
|
235 |
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
|
|
236 |
Collection<ReceiveCommand> filtered = excludePatchsetCommands(commands);
|
|
237 |
super.onPreReceive(rp, filtered);
|
|
238 |
}
|
|
239 |
|
|
240 |
/** Process receive commands EXCEPT for Patchset commands. */
|
|
241 |
@Override
|
|
242 |
public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
|
|
243 |
Collection<ReceiveCommand> filtered = excludePatchsetCommands(commands);
|
|
244 |
super.onPostReceive(rp, filtered);
|
|
245 |
|
|
246 |
// send all queued ticket notifications after processing all patchsets
|
|
247 |
ticketNotifier.sendAll();
|
|
248 |
}
|
|
249 |
|
|
250 |
@Override
|
|
251 |
protected void validateCommands() {
|
|
252 |
// workaround for JGit's awful scoping choices
|
|
253 |
//
|
|
254 |
// set the patchset refs to OK to bypass checks in the super implementation
|
|
255 |
for (final ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) {
|
|
256 |
if (isPatchsetRef(cmd.getRefName())) {
|
|
257 |
if (cmd.getType() == ReceiveCommand.Type.CREATE) {
|
|
258 |
cmd.setResult(Result.OK);
|
|
259 |
}
|
|
260 |
}
|
|
261 |
}
|
|
262 |
|
|
263 |
super.validateCommands();
|
|
264 |
}
|
|
265 |
|
|
266 |
/** Execute commands to update references. */
|
|
267 |
@Override
|
|
268 |
protected void executeCommands() {
|
988334
|
269 |
// we process patchsets unless the user is pushing something special
|
JM |
270 |
boolean processPatchsets = true;
|
|
271 |
for (ReceiveCommand cmd : filterCommands(Result.NOT_ATTEMPTED)) {
|
|
272 |
if (ticketService instanceof BranchTicketService
|
|
273 |
&& BranchTicketService.BRANCH.equals(cmd.getRefName())) {
|
|
274 |
// the user is pushing an update to the BranchTicketService data
|
|
275 |
processPatchsets = false;
|
|
276 |
}
|
|
277 |
}
|
|
278 |
|
5e3521
|
279 |
// workaround for JGit's awful scoping choices
|
JM |
280 |
//
|
|
281 |
// reset the patchset refs to NOT_ATTEMPTED (see validateCommands)
|
|
282 |
for (ReceiveCommand cmd : filterCommands(Result.OK)) {
|
|
283 |
if (isPatchsetRef(cmd.getRefName())) {
|
|
284 |
cmd.setResult(Result.NOT_ATTEMPTED);
|
988334
|
285 |
} else if (ticketService instanceof BranchTicketService
|
JM |
286 |
&& BranchTicketService.BRANCH.equals(cmd.getRefName())) {
|
|
287 |
// the user is pushing an update to the BranchTicketService data
|
|
288 |
processPatchsets = false;
|
5e3521
|
289 |
}
|
JM |
290 |
}
|
|
291 |
|
|
292 |
List<ReceiveCommand> toApply = filterCommands(Result.NOT_ATTEMPTED);
|
|
293 |
if (toApply.isEmpty()) {
|
|
294 |
return;
|
|
295 |
}
|
|
296 |
|
|
297 |
ProgressMonitor updating = NullProgressMonitor.INSTANCE;
|
|
298 |
boolean sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
|
|
299 |
if (sideBand) {
|
|
300 |
SideBandProgressMonitor pm = new SideBandProgressMonitor(msgOut);
|
|
301 |
pm.setDelayStart(250, TimeUnit.MILLISECONDS);
|
|
302 |
updating = pm;
|
|
303 |
}
|
|
304 |
|
|
305 |
BatchRefUpdate batch = getRepository().getRefDatabase().newBatchUpdate();
|
|
306 |
batch.setAllowNonFastForwards(isAllowNonFastForwards());
|
|
307 |
batch.setRefLogIdent(getRefLogIdent());
|
|
308 |
batch.setRefLogMessage("push", true);
|
|
309 |
|
|
310 |
ReceiveCommand patchsetRefCmd = null;
|
|
311 |
PatchsetCommand patchsetCmd = null;
|
|
312 |
for (ReceiveCommand cmd : toApply) {
|
|
313 |
if (Result.NOT_ATTEMPTED != cmd.getResult()) {
|
|
314 |
// Already rejected by the core receive process.
|
|
315 |
continue;
|
|
316 |
}
|
|
317 |
|
988334
|
318 |
if (isPatchsetRef(cmd.getRefName()) && processPatchsets) {
|
f254ee
|
319 |
|
5e3521
|
320 |
if (ticketService == null) {
|
JM |
321 |
sendRejection(cmd, "Sorry, the ticket service is unavailable and can not accept patchsets at this time.");
|
|
322 |
continue;
|
|
323 |
}
|
|
324 |
|
|
325 |
if (!ticketService.isReady()) {
|
|
326 |
sendRejection(cmd, "Sorry, the ticket service can not accept patchsets at this time.");
|
|
327 |
continue;
|
|
328 |
}
|
|
329 |
|
|
330 |
if (UserModel.ANONYMOUS.equals(user)) {
|
|
331 |
// server allows anonymous pushes, but anonymous patchset
|
|
332 |
// contributions are prohibited by design
|
|
333 |
sendRejection(cmd, "Sorry, anonymous patchset contributions are prohibited.");
|
|
334 |
continue;
|
|
335 |
}
|
|
336 |
|
|
337 |
final Matcher m = NEW_PATCHSET.matcher(cmd.getRefName());
|
|
338 |
if (m.matches()) {
|
|
339 |
// prohibit pushing directly to a patchset ref
|
|
340 |
long id = getTicketId(cmd.getRefName());
|
|
341 |
sendError("You may not directly push directly to a patchset ref!");
|
|
342 |
sendError("Instead, please push to one the following:");
|
|
343 |
sendError(" - {0}{1,number,0}", Constants.R_FOR, id);
|
|
344 |
sendError(" - {0}{1,number,0}", Constants.R_TICKET, id);
|
|
345 |
sendRejection(cmd, "protected ref");
|
|
346 |
continue;
|
|
347 |
}
|
|
348 |
|
|
349 |
if (hasRefNamespace(Constants.R_FOR)) {
|
|
350 |
// the refs/for/ namespace exists and it must not
|
|
351 |
LOGGER.error("{} already has refs in the {} namespace",
|
|
352 |
repository.name, Constants.R_FOR);
|
|
353 |
sendRejection(cmd, "Sorry, a repository administrator will have to remove the {} namespace", Constants.R_FOR);
|
|
354 |
continue;
|
|
355 |
}
|
|
356 |
|
f254ee
|
357 |
if (cmd.getNewId().equals(ObjectId.zeroId())) {
|
JM |
358 |
// ref deletion request
|
|
359 |
if (cmd.getRefName().startsWith(Constants.R_TICKET)) {
|
|
360 |
if (user.canDeleteRef(repository)) {
|
|
361 |
batch.addCommand(cmd);
|
|
362 |
} else {
|
|
363 |
sendRejection(cmd, "Sorry, you do not have permission to delete {}", cmd.getRefName());
|
|
364 |
}
|
|
365 |
} else {
|
|
366 |
sendRejection(cmd, "Sorry, you can not delete {}", cmd.getRefName());
|
|
367 |
}
|
|
368 |
continue;
|
|
369 |
}
|
|
370 |
|
5e3521
|
371 |
if (patchsetRefCmd != null) {
|
JM |
372 |
sendRejection(cmd, "You may only push one patchset at a time.");
|
|
373 |
continue;
|
|
374 |
}
|
|
375 |
|
cecc39
|
376 |
LOGGER.info(MessageFormat.format("Verifying {0} push ref \"{1}\" received from {2}",
|
JM |
377 |
repository.name, cmd.getRefName(), user.username));
|
|
378 |
|
5e3521
|
379 |
// responsible verification
|
JM |
380 |
String responsible = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.RESPONSIBLE);
|
|
381 |
if (!StringUtils.isEmpty(responsible)) {
|
|
382 |
UserModel assignee = gitblit.getUserModel(responsible);
|
|
383 |
if (assignee == null) {
|
|
384 |
// no account by this name
|
|
385 |
sendRejection(cmd, "{0} can not be assigned any tickets because there is no user account by that name", responsible);
|
|
386 |
continue;
|
|
387 |
} else if (!assignee.canPush(repository)) {
|
|
388 |
// account does not have RW permissions
|
|
389 |
sendRejection(cmd, "{0} ({1}) can not be assigned any tickets because the user does not have RW permissions for {2}",
|
|
390 |
assignee.getDisplayName(), assignee.username, repository.name);
|
|
391 |
continue;
|
|
392 |
}
|
|
393 |
}
|
|
394 |
|
|
395 |
// milestone verification
|
|
396 |
String milestone = PatchsetCommand.getSingleOption(cmd, PatchsetCommand.MILESTONE);
|
|
397 |
if (!StringUtils.isEmpty(milestone)) {
|
|
398 |
TicketMilestone milestoneModel = ticketService.getMilestone(repository, milestone);
|
|
399 |
if (milestoneModel == null) {
|
|
400 |
// milestone does not exist
|
|
401 |
sendRejection(cmd, "Sorry, \"{0}\" is not a valid milestone!", milestone);
|
|
402 |
continue;
|
|
403 |
}
|
|
404 |
}
|
|
405 |
|
|
406 |
// watcher verification
|
|
407 |
List<String> watchers = PatchsetCommand.getOptions(cmd, PatchsetCommand.WATCH);
|
|
408 |
if (!ArrayUtils.isEmpty(watchers)) {
|
cecc39
|
409 |
boolean verified = true;
|
5e3521
|
410 |
for (String watcher : watchers) {
|
JM |
411 |
UserModel user = gitblit.getUserModel(watcher);
|
|
412 |
if (user == null) {
|
|
413 |
// watcher does not exist
|
|
414 |
sendRejection(cmd, "Sorry, \"{0}\" is not a valid username for the watch list!", watcher);
|
cecc39
|
415 |
verified = false;
|
JM |
416 |
break;
|
5e3521
|
417 |
}
|
cecc39
|
418 |
}
|
JM |
419 |
if (!verified) {
|
|
420 |
continue;
|
5e3521
|
421 |
}
|
JM |
422 |
}
|
|
423 |
|
|
424 |
patchsetRefCmd = cmd;
|
|
425 |
patchsetCmd = preparePatchset(cmd);
|
|
426 |
if (patchsetCmd != null) {
|
|
427 |
batch.addCommand(patchsetCmd);
|
|
428 |
}
|
|
429 |
continue;
|
|
430 |
}
|
|
431 |
|
|
432 |
batch.addCommand(cmd);
|
|
433 |
}
|
|
434 |
|
|
435 |
if (!batch.getCommands().isEmpty()) {
|
|
436 |
try {
|
|
437 |
batch.execute(getRevWalk(), updating);
|
|
438 |
} catch (IOException err) {
|
|
439 |
for (ReceiveCommand cmd : toApply) {
|
|
440 |
if (cmd.getResult() == Result.NOT_ATTEMPTED) {
|
|
441 |
sendRejection(cmd, "lock error: {0}", err.getMessage());
|
988334
|
442 |
LOGGER.error(MessageFormat.format("failed to lock {0}:{1}",
|
JM |
443 |
repository.name, cmd.getRefName()), err);
|
5e3521
|
444 |
}
|
JM |
445 |
}
|
|
446 |
}
|
|
447 |
}
|
|
448 |
|
|
449 |
//
|
|
450 |
// set the results into the patchset ref receive command
|
|
451 |
//
|
|
452 |
if (patchsetRefCmd != null && patchsetCmd != null) {
|
|
453 |
if (!patchsetCmd.getResult().equals(Result.OK)) {
|
|
454 |
// patchset command failed!
|
|
455 |
LOGGER.error(patchsetCmd.getType() + " " + patchsetCmd.getRefName()
|
|
456 |
+ " " + patchsetCmd.getResult());
|
|
457 |
patchsetRefCmd.setResult(patchsetCmd.getResult(), patchsetCmd.getMessage());
|
|
458 |
} else {
|
|
459 |
// all patchset commands were applied
|
|
460 |
patchsetRefCmd.setResult(Result.OK);
|
|
461 |
|
|
462 |
// update the ticket branch ref
|
2dcec5
|
463 |
RefUpdate ru = updateRef(
|
JM |
464 |
patchsetCmd.getTicketBranch(),
|
|
465 |
patchsetCmd.getNewId(),
|
|
466 |
patchsetCmd.getPatchsetType());
|
5e3521
|
467 |
updateReflog(ru);
|
JM |
468 |
|
|
469 |
TicketModel ticket = processPatchset(patchsetCmd);
|
|
470 |
if (ticket != null) {
|
|
471 |
ticketNotifier.queueMailing(ticket);
|
|
472 |
}
|
|
473 |
}
|
|
474 |
}
|
|
475 |
|
|
476 |
//
|
|
477 |
// if there are standard ref update receive commands that were
|
|
478 |
// successfully processed, process referenced tickets, if any
|
|
479 |
//
|
|
480 |
List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK);
|
|
481 |
List<ReceiveCommand> refUpdates = excludePatchsetCommands(allUpdates);
|
|
482 |
List<ReceiveCommand> stdUpdates = excludeTicketCommands(refUpdates);
|
|
483 |
if (!stdUpdates.isEmpty()) {
|
|
484 |
int ticketsProcessed = 0;
|
|
485 |
for (ReceiveCommand cmd : stdUpdates) {
|
|
486 |
switch (cmd.getType()) {
|
|
487 |
case CREATE:
|
|
488 |
case UPDATE:
|
c2188a
|
489 |
if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
|
PM |
490 |
Collection<TicketModel> tickets = processReferencedTickets(cmd);
|
|
491 |
ticketsProcessed += tickets.size();
|
|
492 |
for (TicketModel ticket : tickets) {
|
|
493 |
ticketNotifier.queueMailing(ticket);
|
|
494 |
}
|
|
495 |
}
|
|
496 |
break;
|
|
497 |
|
5e3521
|
498 |
case UPDATE_NONFASTFORWARD:
|
e462bb
|
499 |
if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
|
c2188a
|
500 |
String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
|
PM |
501 |
List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());
|
|
502 |
for (TicketLink link : deletedRefs) {
|
|
503 |
link.isDelete = true;
|
|
504 |
}
|
|
505 |
Change deletion = new Change(user.username);
|
|
506 |
deletion.pendingLinks = deletedRefs;
|
|
507 |
ticketService.updateTicket(repository, 0, deletion);
|
|
508 |
|
|
509 |
Collection<TicketModel> tickets = processReferencedTickets(cmd);
|
e462bb
|
510 |
ticketsProcessed += tickets.size();
|
JM |
511 |
for (TicketModel ticket : tickets) {
|
|
512 |
ticketNotifier.queueMailing(ticket);
|
|
513 |
}
|
5e3521
|
514 |
}
|
JM |
515 |
break;
|
|
516 |
default:
|
|
517 |
break;
|
|
518 |
}
|
|
519 |
}
|
|
520 |
|
|
521 |
if (ticketsProcessed == 1) {
|
|
522 |
sendInfo("1 ticket updated");
|
|
523 |
} else if (ticketsProcessed > 1) {
|
|
524 |
sendInfo("{0} tickets updated", ticketsProcessed);
|
|
525 |
}
|
|
526 |
}
|
|
527 |
|
|
528 |
// reset the ticket caches for the repository
|
|
529 |
ticketService.resetCaches(repository);
|
|
530 |
}
|
|
531 |
|
|
532 |
/**
|
|
533 |
* Prepares a patchset command.
|
|
534 |
*
|
|
535 |
* @param cmd
|
|
536 |
* @return the patchset command
|
|
537 |
*/
|
|
538 |
private PatchsetCommand preparePatchset(ReceiveCommand cmd) {
|
|
539 |
String branch = getIntegrationBranch(cmd.getRefName());
|
|
540 |
long number = getTicketId(cmd.getRefName());
|
|
541 |
|
|
542 |
TicketModel ticket = null;
|
|
543 |
if (number > 0 && ticketService.hasTicket(repository, number)) {
|
|
544 |
ticket = ticketService.getTicket(repository, number);
|
|
545 |
}
|
|
546 |
|
|
547 |
if (ticket == null) {
|
|
548 |
if (number > 0) {
|
|
549 |
// requested ticket does not exist
|
|
550 |
sendError("Sorry, {0} does not have ticket {1,number,0}!", repository.name, number);
|
|
551 |
sendRejection(cmd, "Invalid ticket number");
|
|
552 |
return null;
|
|
553 |
}
|
|
554 |
} else {
|
|
555 |
if (ticket.isMerged()) {
|
|
556 |
// ticket already merged & resolved
|
|
557 |
Change mergeChange = null;
|
|
558 |
for (Change change : ticket.changes) {
|
|
559 |
if (change.isMerge()) {
|
|
560 |
mergeChange = change;
|
|
561 |
break;
|
|
562 |
}
|
|
563 |
}
|
80c3ef
|
564 |
if (mergeChange != null) {
|
JM |
565 |
sendError("Sorry, {0} already merged {1} from ticket {2,number,0} to {3}!",
|
5e3521
|
566 |
mergeChange.author, mergeChange.patchset, number, ticket.mergeTo);
|
80c3ef
|
567 |
}
|
5e3521
|
568 |
sendRejection(cmd, "Ticket {0,number,0} already resolved", number);
|
JM |
569 |
return null;
|
|
570 |
} else if (!StringUtils.isEmpty(ticket.mergeTo)) {
|
|
571 |
// ticket specifies integration branch
|
|
572 |
branch = ticket.mergeTo;
|
|
573 |
}
|
|
574 |
}
|
|
575 |
|
|
576 |
final int shortCommitIdLen = settings.getInteger(Keys.web.shortCommitIdLength, 6);
|
|
577 |
final String shortTipId = cmd.getNewId().getName().substring(0, shortCommitIdLen);
|
|
578 |
final RevCommit tipCommit = JGitUtils.getCommit(getRepository(), cmd.getNewId().getName());
|
|
579 |
final String forBranch = branch;
|
|
580 |
RevCommit mergeBase = null;
|
|
581 |
Ref forBranchRef = getAdvertisedRefs().get(Constants.R_HEADS + forBranch);
|
|
582 |
if (forBranchRef == null || forBranchRef.getObjectId() == null) {
|
|
583 |
// unknown integration branch
|
|
584 |
sendError("Sorry, there is no integration branch named ''{0}''.", forBranch);
|
|
585 |
sendRejection(cmd, "Invalid integration branch specified");
|
|
586 |
return null;
|
|
587 |
} else {
|
|
588 |
// determine the merge base for the patchset on the integration branch
|
|
589 |
String base = JGitUtils.getMergeBase(getRepository(), forBranchRef.getObjectId(), tipCommit.getId());
|
|
590 |
if (StringUtils.isEmpty(base)) {
|
|
591 |
sendError("");
|
|
592 |
sendError("There is no common ancestry between {0} and {1}.", forBranch, shortTipId);
|
|
593 |
sendError("Please reconsider your proposed integration branch, {0}.", forBranch);
|
|
594 |
sendError("");
|
|
595 |
sendRejection(cmd, "no merge base for patchset and {0}", forBranch);
|
|
596 |
return null;
|
|
597 |
}
|
|
598 |
mergeBase = JGitUtils.getCommit(getRepository(), base);
|
|
599 |
}
|
|
600 |
|
|
601 |
// ensure that the patchset can be cleanly merged right now
|
|
602 |
MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch);
|
|
603 |
switch (status) {
|
|
604 |
case ALREADY_MERGED:
|
|
605 |
sendError("");
|
|
606 |
sendError("You have already merged this patchset.", forBranch);
|
|
607 |
sendError("");
|
|
608 |
sendRejection(cmd, "everything up-to-date");
|
|
609 |
return null;
|
|
610 |
case MERGEABLE:
|
|
611 |
break;
|
|
612 |
default:
|
988334
|
613 |
if (ticket == null || requireMergeablePatchset) {
|
5e3521
|
614 |
sendError("");
|
JM |
615 |
sendError("Your patchset can not be cleanly merged into {0}.", forBranch);
|
|
616 |
sendError("Please rebase your patchset and push again.");
|
|
617 |
sendError("NOTE:", number);
|
|
618 |
sendError("You should push your rebase to refs/for/{0,number,0}", number);
|
|
619 |
sendError("");
|
|
620 |
sendError(" git push origin HEAD:refs/for/{0,number,0}", number);
|
|
621 |
sendError("");
|
|
622 |
sendRejection(cmd, "patchset not mergeable");
|
|
623 |
return null;
|
|
624 |
}
|
|
625 |
}
|
c2188a
|
626 |
|
5e3521
|
627 |
// check to see if this commit is already linked to a ticket
|
c2188a
|
628 |
if (ticket != null &&
|
PM |
629 |
JGitUtils.getTicketNumberFromCommitBranch(getRepository(), tipCommit) == ticket.number) {
|
|
630 |
sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, ticket.number);
|
5e3521
|
631 |
sendRejection(cmd, "everything up-to-date");
|
JM |
632 |
return null;
|
|
633 |
}
|
c2188a
|
634 |
|
PM |
635 |
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, tipCommit);
|
|
636 |
|
5e3521
|
637 |
PatchsetCommand psCmd;
|
JM |
638 |
if (ticket == null) {
|
|
639 |
/*
|
|
640 |
* NEW TICKET
|
|
641 |
*/
|
|
642 |
Patchset patchset = newPatchset(null, mergeBase.getName(), tipCommit.getName());
|
|
643 |
|
|
644 |
int minLength = 10;
|
|
645 |
int maxLength = 100;
|
|
646 |
String minTitle = MessageFormat.format(" minimum length of a title is {0} characters.", minLength);
|
|
647 |
String maxTitle = MessageFormat.format(" maximum length of a title is {0} characters.", maxLength);
|
|
648 |
|
|
649 |
if (patchset.commits > 1) {
|
|
650 |
sendError("");
|
2e73ef
|
651 |
sendError("You may not create a ''{0}'' branch proposal ticket from {1} commits!",
|
JM |
652 |
forBranch, patchset.commits);
|
5e3521
|
653 |
sendError("");
|
2e73ef
|
654 |
// display an ellipsized log of the commits being pushed
|
JM |
655 |
RevWalk walk = getRevWalk();
|
|
656 |
walk.reset();
|
|
657 |
walk.sort(RevSort.TOPO);
|
|
658 |
int boundary = 3;
|
|
659 |
int count = 0;
|
|
660 |
try {
|
|
661 |
walk.markStart(tipCommit);
|
|
662 |
walk.markUninteresting(mergeBase);
|
|
663 |
|
|
664 |
for (;;) {
|
|
665 |
|
|
666 |
RevCommit c = walk.next();
|
|
667 |
if (c == null) {
|
|
668 |
break;
|
|
669 |
}
|
|
670 |
|
|
671 |
if (count < boundary || count >= (patchset.commits - boundary)) {
|
|
672 |
|
|
673 |
walk.parseBody(c);
|
|
674 |
sendError(" {0} {1}", c.getName().substring(0, shortCommitIdLen),
|
|
675 |
StringUtils.trimString(c.getShortMessage(), 60));
|
|
676 |
|
|
677 |
} else if (count == boundary) {
|
|
678 |
|
|
679 |
sendError(" ... more commits ...");
|
|
680 |
|
|
681 |
}
|
|
682 |
|
|
683 |
count++;
|
|
684 |
}
|
|
685 |
|
|
686 |
} catch (IOException e) {
|
|
687 |
// Should never happen, the core receive process would have
|
|
688 |
// identified the missing object earlier before we got control.
|
|
689 |
LOGGER.error("failed to get commit count", e);
|
|
690 |
} finally {
|
a1cee6
|
691 |
walk.close();
|
2e73ef
|
692 |
}
|
JM |
693 |
|
5e3521
|
694 |
sendError("");
|
2e73ef
|
695 |
sendError("Possible Solutions:");
|
JM |
696 |
sendError("");
|
|
697 |
int solution = 1;
|
|
698 |
String forSpec = cmd.getRefName().substring(Constants.R_FOR.length());
|
|
699 |
if (forSpec.equals("default") || forSpec.equals("new")) {
|
|
700 |
try {
|
|
701 |
// determine other possible integration targets
|
|
702 |
List<String> bases = Lists.newArrayList();
|
|
703 |
for (Ref ref : getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values()) {
|
|
704 |
if (!ref.getName().startsWith(Constants.R_TICKET)
|
|
705 |
&& !ref.getName().equals(forBranchRef.getName())) {
|
|
706 |
if (JGitUtils.isMergedInto(getRepository(), ref.getObjectId(), tipCommit)) {
|
|
707 |
bases.add(Repository.shortenRefName(ref.getName()));
|
|
708 |
}
|
|
709 |
}
|
|
710 |
}
|
|
711 |
|
|
712 |
if (!bases.isEmpty()) {
|
|
713 |
|
|
714 |
if (bases.size() == 1) {
|
|
715 |
// suggest possible integration targets
|
|
716 |
String base = bases.get(0);
|
|
717 |
sendError("{0}. Propose this change for the ''{1}'' branch.", solution++, base);
|
|
718 |
sendError("");
|
|
719 |
sendError(" git push origin HEAD:refs/for/{0}", base);
|
|
720 |
sendError(" pt propose {0}", base);
|
|
721 |
sendError("");
|
|
722 |
} else {
|
|
723 |
// suggest possible integration targets
|
|
724 |
sendError("{0}. Propose this change for a different branch.", solution++);
|
|
725 |
sendError("");
|
|
726 |
for (String base : bases) {
|
|
727 |
sendError(" git push origin HEAD:refs/for/{0}", base);
|
|
728 |
sendError(" pt propose {0}", base);
|
|
729 |
sendError("");
|
|
730 |
}
|
|
731 |
}
|
|
732 |
|
|
733 |
}
|
|
734 |
} catch (IOException e) {
|
|
735 |
LOGGER.error(null, e);
|
|
736 |
}
|
|
737 |
}
|
|
738 |
sendError("{0}. Squash your changes into a single commit with a meaningful message.", solution++);
|
|
739 |
sendError("");
|
|
740 |
sendError("{0}. Open a ticket for your changes and then push your {1} commits to the ticket.",
|
|
741 |
solution++, patchset.commits);
|
|
742 |
sendError("");
|
|
743 |
sendError(" git push origin HEAD:refs/for/{id}");
|
|
744 |
sendError(" pt propose {id}");
|
|
745 |
sendError("");
|
|
746 |
sendRejection(cmd, "too many commits");
|
5e3521
|
747 |
return null;
|
JM |
748 |
}
|
|
749 |
|
|
750 |
// require a reasonable title/subject
|
|
751 |
String title = tipCommit.getFullMessage().trim().split("\n")[0];
|
|
752 |
if (title.length() < minLength) {
|
|
753 |
// reject, title too short
|
|
754 |
sendError("");
|
|
755 |
sendError("Please supply a longer title in your commit message!");
|
|
756 |
sendError("");
|
|
757 |
sendError(minTitle);
|
|
758 |
sendError(maxTitle);
|
|
759 |
sendError("");
|
|
760 |
sendRejection(cmd, "ticket title is too short [{0}/{1}]", title.length(), maxLength);
|
|
761 |
return null;
|
|
762 |
}
|
|
763 |
if (title.length() > maxLength) {
|
|
764 |
// reject, title too long
|
|
765 |
sendError("");
|
|
766 |
sendError("Please supply a more concise title in your commit message!");
|
|
767 |
sendError("");
|
|
768 |
sendError(minTitle);
|
|
769 |
sendError(maxTitle);
|
|
770 |
sendError("");
|
|
771 |
sendRejection(cmd, "ticket title is too long [{0}/{1}]", title.length(), maxLength);
|
|
772 |
return null;
|
|
773 |
}
|
|
774 |
|
|
775 |
// assign new id
|
|
776 |
long ticketId = ticketService.assignNewId(repository);
|
|
777 |
|
|
778 |
// create the patchset command
|
|
779 |
psCmd = new PatchsetCommand(user.username, patchset);
|
|
780 |
psCmd.newTicket(tipCommit, forBranch, ticketId, cmd.getRefName());
|
|
781 |
} else {
|
|
782 |
/*
|
|
783 |
* EXISTING TICKET
|
|
784 |
*/
|
|
785 |
Patchset patchset = newPatchset(ticket, mergeBase.getName(), tipCommit.getName());
|
|
786 |
psCmd = new PatchsetCommand(user.username, patchset);
|
|
787 |
psCmd.updateTicket(tipCommit, forBranch, ticket, cmd.getRefName());
|
|
788 |
}
|
|
789 |
|
|
790 |
// confirm user can push the patchset
|
|
791 |
boolean pushPermitted = ticket == null
|
|
792 |
|| !ticket.hasPatchsets()
|
|
793 |
|| ticket.isAuthor(user.username)
|
|
794 |
|| ticket.isPatchsetAuthor(user.username)
|
|
795 |
|| ticket.isResponsible(user.username)
|
|
796 |
|| user.canPush(repository);
|
|
797 |
|
|
798 |
switch (psCmd.getPatchsetType()) {
|
|
799 |
case Proposal:
|
|
800 |
// proposals (first patchset) are always acceptable
|
|
801 |
break;
|
|
802 |
case FastForward:
|
|
803 |
// patchset updates must be permitted
|
|
804 |
if (!pushPermitted) {
|
|
805 |
// reject
|
|
806 |
sendError("");
|
|
807 |
sendError("To push a patchset to this ticket one of the following must be true:");
|
|
808 |
sendError(" 1. you created the ticket");
|
|
809 |
sendError(" 2. you created the first patchset");
|
|
810 |
sendError(" 3. you are specified as responsible for the ticket");
|
6d298c
|
811 |
sendError(" 4. you have push (RW) permissions to {0}", repository.name);
|
5e3521
|
812 |
sendError("");
|
JM |
813 |
sendRejection(cmd, "not permitted to push to ticket {0,number,0}", ticket.number);
|
|
814 |
return null;
|
|
815 |
}
|
|
816 |
break;
|
|
817 |
default:
|
|
818 |
// non-fast-forward push
|
|
819 |
if (!pushPermitted) {
|
|
820 |
// reject
|
|
821 |
sendRejection(cmd, "non-fast-forward ({0})", psCmd.getPatchsetType());
|
|
822 |
return null;
|
|
823 |
}
|
|
824 |
break;
|
|
825 |
}
|
c2188a
|
826 |
|
PM |
827 |
Change change = psCmd.getChange();
|
|
828 |
change.pendingLinks = ticketLinks;
|
|
829 |
|
5e3521
|
830 |
return psCmd;
|
JM |
831 |
}
|
|
832 |
|
|
833 |
/**
|
|
834 |
* Creates or updates an ticket with the specified patchset.
|
|
835 |
*
|
|
836 |
* @param cmd
|
|
837 |
* @return a ticket if the creation or update was successful
|
|
838 |
*/
|
|
839 |
private TicketModel processPatchset(PatchsetCommand cmd) {
|
|
840 |
Change change = cmd.getChange();
|
|
841 |
|
|
842 |
if (cmd.isNewTicket()) {
|
|
843 |
// create the ticket object
|
|
844 |
TicketModel ticket = ticketService.createTicket(repository, cmd.getTicketId(), change);
|
|
845 |
if (ticket != null) {
|
|
846 |
sendInfo("");
|
|
847 |
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
|
|
848 |
sendInfo("created proposal ticket from patchset");
|
|
849 |
sendInfo(ticketService.getTicketUrl(ticket));
|
|
850 |
sendInfo("");
|
|
851 |
|
|
852 |
// log the new patch ref
|
|
853 |
RefLogUtils.updateRefLog(user, getRepository(),
|
|
854 |
Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName())));
|
|
855 |
|
aa89be
|
856 |
// call any patchset hooks
|
JM |
857 |
for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
|
|
858 |
try {
|
|
859 |
hook.onNewPatchset(ticket);
|
|
860 |
} catch (Exception e) {
|
|
861 |
LOGGER.error("Failed to execute extension", e);
|
|
862 |
}
|
|
863 |
}
|
|
864 |
|
5e3521
|
865 |
return ticket;
|
JM |
866 |
} else {
|
|
867 |
sendError("FAILED to create ticket");
|
|
868 |
}
|
|
869 |
} else {
|
|
870 |
// update an existing ticket
|
|
871 |
TicketModel ticket = ticketService.updateTicket(repository, cmd.getTicketId(), change);
|
|
872 |
if (ticket != null) {
|
|
873 |
sendInfo("");
|
|
874 |
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
|
|
875 |
if (change.patchset.rev == 1) {
|
|
876 |
// new patchset
|
|
877 |
sendInfo("uploaded patchset {0} ({1})", change.patchset.number, change.patchset.type.toString());
|
|
878 |
} else {
|
|
879 |
// updated patchset
|
|
880 |
sendInfo("added {0} {1} to patchset {2}",
|
|
881 |
change.patchset.added,
|
|
882 |
change.patchset.added == 1 ? "commit" : "commits",
|
|
883 |
change.patchset.number);
|
|
884 |
}
|
|
885 |
sendInfo(ticketService.getTicketUrl(ticket));
|
|
886 |
sendInfo("");
|
|
887 |
|
|
888 |
// log the new patchset ref
|
|
889 |
RefLogUtils.updateRefLog(user, getRepository(),
|
|
890 |
Arrays.asList(new ReceiveCommand(cmd.getOldId(), cmd.getNewId(), cmd.getRefName())));
|
aa89be
|
891 |
|
JM |
892 |
// call any patchset hooks
|
|
893 |
final boolean isNewPatchset = change.patchset.rev == 1;
|
|
894 |
for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
|
|
895 |
try {
|
|
896 |
if (isNewPatchset) {
|
|
897 |
hook.onNewPatchset(ticket);
|
|
898 |
} else {
|
|
899 |
hook.onUpdatePatchset(ticket);
|
|
900 |
}
|
|
901 |
} catch (Exception e) {
|
|
902 |
LOGGER.error("Failed to execute extension", e);
|
|
903 |
}
|
|
904 |
}
|
5e3521
|
905 |
|
JM |
906 |
// return the updated ticket
|
|
907 |
return ticket;
|
|
908 |
} else {
|
|
909 |
sendError("FAILED to upload {0} for ticket {1,number,0}", change.patchset, cmd.getTicketId());
|
|
910 |
}
|
|
911 |
}
|
|
912 |
|
|
913 |
return null;
|
|
914 |
}
|
|
915 |
|
|
916 |
/**
|
|
917 |
* Automatically closes open tickets that have been merged to their integration
|
c2188a
|
918 |
* branch by a client and adds references to tickets if made in the commit message.
|
5e3521
|
919 |
*
|
JM |
920 |
* @param cmd
|
|
921 |
*/
|
c2188a
|
922 |
private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
|
5e3521
|
923 |
Map<Long, TicketModel> mergedTickets = new LinkedHashMap<Long, TicketModel>();
|
JM |
924 |
final RevWalk rw = getRevWalk();
|
|
925 |
try {
|
|
926 |
rw.reset();
|
|
927 |
rw.markStart(rw.parseCommit(cmd.getNewId()));
|
|
928 |
if (!ObjectId.zeroId().equals(cmd.getOldId())) {
|
|
929 |
rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
|
|
930 |
}
|
|
931 |
|
|
932 |
RevCommit c;
|
|
933 |
while ((c = rw.next()) != null) {
|
|
934 |
rw.parseBody(c);
|
c2188a
|
935 |
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
|
PM |
936 |
if (ticketLinks == null) {
|
5e3521
|
937 |
continue;
|
JM |
938 |
}
|
|
939 |
|
c2188a
|
940 |
for (TicketLink link : ticketLinks) {
|
PM |
941 |
|
|
942 |
if (mergedTickets.containsKey(link.targetTicketId)) {
|
5e3521
|
943 |
continue;
|
JM |
944 |
}
|
c2188a
|
945 |
|
PM |
946 |
TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
|
|
947 |
if (ticket == null) {
|
|
948 |
continue;
|
|
949 |
}
|
|
950 |
String integrationBranch;
|
|
951 |
if (StringUtils.isEmpty(ticket.mergeTo)) {
|
|
952 |
// unspecified integration branch
|
|
953 |
integrationBranch = null;
|
|
954 |
} else {
|
|
955 |
// specified integration branch
|
|
956 |
integrationBranch = Constants.R_HEADS + ticket.mergeTo;
|
|
957 |
}
|
|
958 |
|
|
959 |
Change change;
|
|
960 |
Patchset patchset = null;
|
|
961 |
String mergeSha = c.getName();
|
|
962 |
String mergeTo = Repository.shortenRefName(cmd.getRefName());
|
5e3521
|
963 |
|
c2188a
|
964 |
if (link.action == TicketAction.Commit) {
|
PM |
965 |
//A commit can reference a ticket in any branch even if the ticket is closed.
|
|
966 |
//This allows developers to identify and communicate related issues
|
|
967 |
change = new Change(user.username);
|
|
968 |
change.referenceCommit(mergeSha);
|
|
969 |
} else {
|
|
970 |
// ticket must be open and, if specified, the ref must match the integration branch
|
|
971 |
if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {
|
|
972 |
continue;
|
|
973 |
}
|
|
974 |
|
|
975 |
String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);
|
|
976 |
boolean knownPatchset = false;
|
|
977 |
Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
|
|
978 |
if (refs != null) {
|
|
979 |
for (Ref ref : refs) {
|
|
980 |
if (ref.getName().startsWith(baseRef)) {
|
|
981 |
knownPatchset = true;
|
|
982 |
break;
|
|
983 |
}
|
|
984 |
}
|
|
985 |
}
|
|
986 |
|
|
987 |
if (knownPatchset) {
|
|
988 |
// identify merged patchset by the patchset tip
|
|
989 |
for (Patchset ps : ticket.getPatchsets()) {
|
|
990 |
if (ps.tip.equals(mergeSha)) {
|
|
991 |
patchset = ps;
|
|
992 |
break;
|
|
993 |
}
|
|
994 |
}
|
|
995 |
|
|
996 |
if (patchset == null) {
|
|
997 |
// should not happen - unless ticket has been hacked
|
|
998 |
sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",
|
|
999 |
mergeSha, ticket.number);
|
|
1000 |
continue;
|
|
1001 |
}
|
|
1002 |
|
|
1003 |
// create a new change
|
|
1004 |
change = new Change(user.username);
|
|
1005 |
} else {
|
|
1006 |
// new patchset pushed by user
|
|
1007 |
String base = cmd.getOldId().getName();
|
|
1008 |
patchset = newPatchset(ticket, base, mergeSha);
|
|
1009 |
PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);
|
|
1010 |
psCmd.updateTicket(c, mergeTo, ticket, null);
|
|
1011 |
|
|
1012 |
// create a ticket patchset ref
|
|
1013 |
updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);
|
|
1014 |
RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);
|
|
1015 |
updateReflog(ru);
|
|
1016 |
|
|
1017 |
// create a change from the patchset command
|
|
1018 |
change = psCmd.getChange();
|
|
1019 |
}
|
|
1020 |
|
|
1021 |
// set the common change data about the merge
|
|
1022 |
change.setField(Field.status, Status.Merged);
|
|
1023 |
change.setField(Field.mergeSha, mergeSha);
|
|
1024 |
change.setField(Field.mergeTo, mergeTo);
|
|
1025 |
|
|
1026 |
if (StringUtils.isEmpty(ticket.responsible)) {
|
|
1027 |
// unassigned tickets are assigned to the closer
|
|
1028 |
change.setField(Field.responsible, user.username);
|
|
1029 |
}
|
|
1030 |
}
|
|
1031 |
|
|
1032 |
ticket = ticketService.updateTicket(repository, ticket.number, change);
|
|
1033 |
|
|
1034 |
if (ticket != null) {
|
|
1035 |
sendInfo("");
|
|
1036 |
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
|
5e3521
|
1037 |
|
c2188a
|
1038 |
switch (link.action) {
|
PM |
1039 |
case Commit: {
|
|
1040 |
sendInfo("referenced by push of {0} to {1}", c.getName(), mergeTo);
|
|
1041 |
}
|
|
1042 |
break;
|
5e3521
|
1043 |
|
c2188a
|
1044 |
case Close: {
|
PM |
1045 |
sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
|
|
1046 |
mergedTickets.put(ticket.number, ticket);
|
|
1047 |
}
|
|
1048 |
break;
|
5e3521
|
1049 |
|
c2188a
|
1050 |
default: {
|
PM |
1051 |
|
|
1052 |
}
|
|
1053 |
}
|
5e3521
|
1054 |
|
c2188a
|
1055 |
sendInfo(ticketService.getTicketUrl(ticket));
|
PM |
1056 |
sendInfo("");
|
5e3521
|
1057 |
|
c2188a
|
1058 |
} else {
|
PM |
1059 |
String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
|
|
1060 |
|
|
1061 |
switch (link.action) {
|
|
1062 |
case Commit: {
|
|
1063 |
sendError("FAILED to reference ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);
|
|
1064 |
}
|
|
1065 |
break;
|
|
1066 |
case Close: {
|
|
1067 |
sendError("FAILED to close ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);
|
|
1068 |
} break;
|
|
1069 |
|
|
1070 |
default: {
|
|
1071 |
|
|
1072 |
}
|
|
1073 |
}
|
|
1074 |
}
|
5e3521
|
1075 |
}
|
JM |
1076 |
}
|
c2188a
|
1077 |
|
5e3521
|
1078 |
} catch (IOException e) {
|
c2188a
|
1079 |
LOGGER.error("Can't scan for changes to reference or close", e);
|
5e3521
|
1080 |
} finally {
|
JM |
1081 |
rw.reset();
|
|
1082 |
}
|
|
1083 |
|
|
1084 |
return mergedTickets.values();
|
|
1085 |
}
|
|
1086 |
|
c2188a
|
1087 |
|
5e3521
|
1088 |
|
c2188a
|
1089 |
|
5e3521
|
1090 |
|
JM |
1091 |
/**
|
|
1092 |
* Creates a new patchset with metadata.
|
|
1093 |
*
|
|
1094 |
* @param ticket
|
|
1095 |
* @param mergeBase
|
|
1096 |
* @param tip
|
|
1097 |
*/
|
|
1098 |
private Patchset newPatchset(TicketModel ticket, String mergeBase, String tip) {
|
c2188a
|
1099 |
int totalCommits = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, tip);
|
5e3521
|
1100 |
|
JM |
1101 |
Patchset newPatchset = new Patchset();
|
|
1102 |
newPatchset.tip = tip;
|
|
1103 |
newPatchset.base = mergeBase;
|
|
1104 |
newPatchset.commits = totalCommits;
|
|
1105 |
|
|
1106 |
Patchset currPatchset = ticket == null ? null : ticket.getCurrentPatchset();
|
|
1107 |
if (currPatchset == null) {
|
|
1108 |
/*
|
|
1109 |
* PROPOSAL PATCHSET
|
|
1110 |
* patchset 1, rev 1
|
|
1111 |
*/
|
|
1112 |
newPatchset.number = 1;
|
|
1113 |
newPatchset.rev = 1;
|
|
1114 |
newPatchset.type = PatchsetType.Proposal;
|
|
1115 |
|
|
1116 |
// diffstat from merge base
|
|
1117 |
DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
|
|
1118 |
newPatchset.insertions = diffStat.getInsertions();
|
|
1119 |
newPatchset.deletions = diffStat.getDeletions();
|
|
1120 |
} else {
|
|
1121 |
/*
|
|
1122 |
* PATCHSET UPDATE
|
|
1123 |
*/
|
|
1124 |
int added = totalCommits - currPatchset.commits;
|
|
1125 |
boolean ff = JGitUtils.isMergedInto(getRepository(), currPatchset.tip, tip);
|
|
1126 |
boolean squash = added < 0;
|
|
1127 |
boolean rebase = !currPatchset.base.equals(mergeBase);
|
|
1128 |
|
|
1129 |
// determine type, number and rev of the patchset
|
|
1130 |
if (ff) {
|
|
1131 |
/*
|
|
1132 |
* FAST-FORWARD
|
|
1133 |
* patchset number preserved, rev incremented
|
|
1134 |
*/
|
|
1135 |
|
|
1136 |
boolean merged = JGitUtils.isMergedInto(getRepository(), currPatchset.tip, ticket.mergeTo);
|
|
1137 |
if (merged) {
|
|
1138 |
// current patchset was already merged
|
|
1139 |
// new patchset, mark as rebase
|
|
1140 |
newPatchset.type = PatchsetType.Rebase;
|
|
1141 |
newPatchset.number = currPatchset.number + 1;
|
|
1142 |
newPatchset.rev = 1;
|
|
1143 |
|
|
1144 |
// diffstat from parent
|
|
1145 |
DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
|
|
1146 |
newPatchset.insertions = diffStat.getInsertions();
|
|
1147 |
newPatchset.deletions = diffStat.getDeletions();
|
|
1148 |
} else {
|
|
1149 |
// FF update to patchset
|
|
1150 |
newPatchset.type = PatchsetType.FastForward;
|
|
1151 |
newPatchset.number = currPatchset.number;
|
|
1152 |
newPatchset.rev = currPatchset.rev + 1;
|
|
1153 |
newPatchset.parent = currPatchset.tip;
|
|
1154 |
|
|
1155 |
// diffstat from parent
|
|
1156 |
DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), currPatchset.tip, tip);
|
|
1157 |
newPatchset.insertions = diffStat.getInsertions();
|
|
1158 |
newPatchset.deletions = diffStat.getDeletions();
|
|
1159 |
}
|
|
1160 |
} else {
|
|
1161 |
/*
|
|
1162 |
* NON-FAST-FORWARD
|
|
1163 |
* new patchset, rev 1
|
|
1164 |
*/
|
|
1165 |
if (rebase && squash) {
|
|
1166 |
newPatchset.type = PatchsetType.Rebase_Squash;
|
|
1167 |
newPatchset.number = currPatchset.number + 1;
|
|
1168 |
newPatchset.rev = 1;
|
|
1169 |
} else if (squash) {
|
|
1170 |
newPatchset.type = PatchsetType.Squash;
|
|
1171 |
newPatchset.number = currPatchset.number + 1;
|
|
1172 |
newPatchset.rev = 1;
|
|
1173 |
} else if (rebase) {
|
|
1174 |
newPatchset.type = PatchsetType.Rebase;
|
|
1175 |
newPatchset.number = currPatchset.number + 1;
|
|
1176 |
newPatchset.rev = 1;
|
|
1177 |
} else {
|
|
1178 |
newPatchset.type = PatchsetType.Amend;
|
|
1179 |
newPatchset.number = currPatchset.number + 1;
|
|
1180 |
newPatchset.rev = 1;
|
|
1181 |
}
|
|
1182 |
|
|
1183 |
// diffstat from merge base
|
|
1184 |
DiffStat diffStat = DiffUtils.getDiffStat(getRepository(), mergeBase, tip);
|
|
1185 |
newPatchset.insertions = diffStat.getInsertions();
|
|
1186 |
newPatchset.deletions = diffStat.getDeletions();
|
|
1187 |
}
|
|
1188 |
|
|
1189 |
if (added > 0) {
|
|
1190 |
// ignore squash (negative add)
|
|
1191 |
newPatchset.added = added;
|
|
1192 |
}
|
|
1193 |
}
|
|
1194 |
|
|
1195 |
return newPatchset;
|
|
1196 |
}
|
|
1197 |
|
2dcec5
|
1198 |
private RefUpdate updateRef(String ref, ObjectId newId, PatchsetType type) {
|
5e3521
|
1199 |
ObjectId ticketRefId = ObjectId.zeroId();
|
JM |
1200 |
try {
|
|
1201 |
ticketRefId = getRepository().resolve(ref);
|
|
1202 |
} catch (Exception e) {
|
|
1203 |
// ignore
|
|
1204 |
}
|
|
1205 |
|
|
1206 |
try {
|
|
1207 |
RefUpdate ru = getRepository().updateRef(ref, false);
|
|
1208 |
ru.setRefLogIdent(getRefLogIdent());
|
2dcec5
|
1209 |
switch (type) {
|
JM |
1210 |
case Amend:
|
|
1211 |
case Rebase:
|
|
1212 |
case Rebase_Squash:
|
|
1213 |
case Squash:
|
|
1214 |
ru.setForceUpdate(true);
|
|
1215 |
break;
|
|
1216 |
default:
|
|
1217 |
break;
|
|
1218 |
}
|
|
1219 |
|
5e3521
|
1220 |
ru.setExpectedOldObjectId(ticketRefId);
|
JM |
1221 |
ru.setNewObjectId(newId);
|
|
1222 |
RefUpdate.Result result = ru.update(getRevWalk());
|
|
1223 |
if (result == RefUpdate.Result.LOCK_FAILURE) {
|
|
1224 |
sendError("Failed to obtain lock when updating {0}:{1}", repository.name, ref);
|
|
1225 |
sendError("Perhaps an administrator should remove {0}/{1}.lock?", getRepository().getDirectory(), ref);
|
|
1226 |
return null;
|
|
1227 |
}
|
|
1228 |
return ru;
|
|
1229 |
} catch (IOException e) {
|
|
1230 |
LOGGER.error("failed to update ref " + ref, e);
|
|
1231 |
sendError("There was an error updating ref {0}:{1}", repository.name, ref);
|
|
1232 |
}
|
|
1233 |
return null;
|
|
1234 |
}
|
|
1235 |
|
|
1236 |
private void updateReflog(RefUpdate ru) {
|
|
1237 |
if (ru == null) {
|
|
1238 |
return;
|
|
1239 |
}
|
|
1240 |
|
|
1241 |
ReceiveCommand.Type type = null;
|
|
1242 |
switch (ru.getResult()) {
|
|
1243 |
case NEW:
|
|
1244 |
type = Type.CREATE;
|
|
1245 |
break;
|
|
1246 |
case FAST_FORWARD:
|
|
1247 |
type = Type.UPDATE;
|
|
1248 |
break;
|
|
1249 |
case FORCED:
|
|
1250 |
type = Type.UPDATE_NONFASTFORWARD;
|
|
1251 |
break;
|
|
1252 |
default:
|
|
1253 |
LOGGER.error(MessageFormat.format("unexpected ref update type {0} for {1}",
|
|
1254 |
ru.getResult(), ru.getName()));
|
|
1255 |
return;
|
|
1256 |
}
|
|
1257 |
ReceiveCommand cmd = new ReceiveCommand(ru.getOldObjectId(), ru.getNewObjectId(), ru.getName(), type);
|
|
1258 |
RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(cmd));
|
|
1259 |
}
|
|
1260 |
|
|
1261 |
/**
|
|
1262 |
* Merge the specified patchset to the integration branch.
|
|
1263 |
*
|
|
1264 |
* @param ticket
|
|
1265 |
* @param patchset
|
|
1266 |
* @return true, if successful
|
|
1267 |
*/
|
|
1268 |
public MergeStatus merge(TicketModel ticket) {
|
|
1269 |
PersonIdent committer = new PersonIdent(user.getDisplayName(), StringUtils.isEmpty(user.emailAddress) ? (user.username + "@gitblit") : user.emailAddress);
|
|
1270 |
Patchset patchset = ticket.getCurrentPatchset();
|
|
1271 |
String message = MessageFormat.format("Merged #{0,number,0} \"{1}\"", ticket.number, ticket.title);
|
|
1272 |
Ref oldRef = null;
|
|
1273 |
try {
|
|
1274 |
oldRef = getRepository().getRef(ticket.mergeTo);
|
|
1275 |
} catch (IOException e) {
|
|
1276 |
LOGGER.error("failed to get ref for " + ticket.mergeTo, e);
|
|
1277 |
}
|
|
1278 |
MergeResult mergeResult = JGitUtils.merge(
|
|
1279 |
getRepository(),
|
|
1280 |
patchset.tip,
|
|
1281 |
ticket.mergeTo,
|
|
1282 |
committer,
|
|
1283 |
message);
|
|
1284 |
|
|
1285 |
if (StringUtils.isEmpty(mergeResult.sha)) {
|
|
1286 |
LOGGER.error("FAILED to merge {} to {} ({})", new Object [] { patchset, ticket.mergeTo, mergeResult.status.name() });
|
|
1287 |
return mergeResult.status;
|
|
1288 |
}
|
|
1289 |
Change change = new Change(user.username);
|
|
1290 |
change.setField(Field.status, Status.Merged);
|
|
1291 |
change.setField(Field.mergeSha, mergeResult.sha);
|
|
1292 |
change.setField(Field.mergeTo, ticket.mergeTo);
|
|
1293 |
|
|
1294 |
if (StringUtils.isEmpty(ticket.responsible)) {
|
|
1295 |
// unassigned tickets are assigned to the closer
|
|
1296 |
change.setField(Field.responsible, user.username);
|
|
1297 |
}
|
|
1298 |
|
|
1299 |
long ticketId = ticket.number;
|
|
1300 |
ticket = ticketService.updateTicket(repository, ticket.number, change);
|
|
1301 |
if (ticket != null) {
|
|
1302 |
ticketNotifier.queueMailing(ticket);
|
|
1303 |
|
|
1304 |
if (oldRef != null) {
|
|
1305 |
ReceiveCommand cmd = new ReceiveCommand(oldRef.getObjectId(),
|
|
1306 |
ObjectId.fromString(mergeResult.sha), oldRef.getName());
|
d62d88
|
1307 |
cmd.setResult(Result.OK);
|
JM |
1308 |
List<ReceiveCommand> commands = Arrays.asList(cmd);
|
|
1309 |
|
|
1310 |
logRefChange(commands);
|
|
1311 |
updateIncrementalPushTags(commands);
|
|
1312 |
updateGitblitRefLog(commands);
|
5e3521
|
1313 |
}
|
aa89be
|
1314 |
|
JM |
1315 |
// call patchset hooks
|
|
1316 |
for (PatchsetHook hook : gitblit.getExtensions(PatchsetHook.class)) {
|
|
1317 |
try {
|
|
1318 |
hook.onMergePatchset(ticket);
|
|
1319 |
} catch (Exception e) {
|
|
1320 |
LOGGER.error("Failed to execute extension", e);
|
|
1321 |
}
|
|
1322 |
}
|
5e3521
|
1323 |
return mergeResult.status;
|
JM |
1324 |
} else {
|
|
1325 |
LOGGER.error("FAILED to resolve ticket {} by merge from web ui", ticketId);
|
|
1326 |
}
|
|
1327 |
return mergeResult.status;
|
|
1328 |
}
|
|
1329 |
|
|
1330 |
public void sendAll() {
|
|
1331 |
ticketNotifier.sendAll();
|
|
1332 |
}
|
|
1333 |
} |