-
-
Notifications
You must be signed in to change notification settings - Fork 273
Expand file tree
/
Copy pathSubversionRepositoryStatus.java
More file actions
executable file
·327 lines (277 loc) · 14 KB
/
Copy pathSubversionRepositoryStatus.java
File metadata and controls
executable file
·327 lines (277 loc) · 14 KB
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
package hudson.scm;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.FINER;
import static java.util.logging.Level.WARNING;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.AbstractProject;
import hudson.model.Job;
import hudson.scm.SubversionSCM.ModuleLocation;
import hudson.scm.SubversionSCM.SvnInfo;
import hudson.triggers.SCMTrigger;
import hudson.util.QueryParameterMap;
import java.io.BufferedReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import jenkins.model.Jenkins;
import jenkins.triggers.SCMTriggerItem;
import org.apache.commons.io.IOUtils;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.tmatesoft.svn.core.SVNCancelException;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.SVNException;
/**
* Per repository status.
* <p>
* Receives post-commit hook notifications.
*
* @author Kohsuke Kawaguchi
* @see SubversionStatus
*/
public class SubversionRepositoryStatus {
public final UUID uuid;
public SubversionRepositoryStatus(UUID uuid) {
this.uuid = uuid;
}
public String getDisplayName() {
return uuid.toString();
}
static interface JobProvider {
@SuppressWarnings("rawtypes")
List<Job> getAllJobs();
}
/**
* An extension point to allow things other than jobs to listen for repository status updates.
*/
public static abstract class Listener implements ExtensionPoint {
/**
* Called when a post-commit hook notification has been received.
* @param uuid the UUID of the repository against which the hook was received.
* @param revision the revision (if known) or {@code -1} if unknown.
* @return {@code true} if a match for the UUID was found and something was scheduled as a result.
*/
public abstract boolean onNotify(UUID uuid, long revision, Set<String> affectedPaths);
}
private static Method IS_IGNORE_POST_COMMIT_HOOKS_METHOD;
/**
* Notify the commit to this repository.
*
* <p>
* Because this URL is not guarded, we can't really trust the data that's sent to us. But we intentionally
* don't protect this URL to simplify <code>post-commit</code> script set up.
*/
@RequirePOST
public void doNotifyCommit(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
// compute the affected paths
Set<String> affectedPath = new HashSet<>();
String line;
BufferedReader r = new BufferedReader(req.getReader());
try {
while((line=r.readLine())!=null) {
if (LOGGER.isLoggable(FINER)) {
LOGGER.finer("Reading line: "+line);
}
affectedPath.add(line.substring(4));
if (line.startsWith("svnlook changed --revision ")) {
String msg = "Expecting the output from the svnlook command but instead you just sent me the svnlook invocation command line: " + line;
LOGGER.warning(msg);
throw new IllegalArgumentException(msg);
}
}
} finally {
IOUtils.closeQuietly(r);
}
if(LOGGER.isLoggable(FINE))
LOGGER.fine("Change reported to Subversion repository "+uuid+" on "+affectedPath);
// we can't reliably use req.getParameter() as it can try to parse the payload, which we've already consumed above.
// servlet container relies on Content-type to decide if it wants to parse the payload or not, and at least
// in case of Jetty, it doesn't check if the payload is
QueryParameterMap query = new QueryParameterMap(req);
String revParam = query.get("rev");
if (revParam == null) {
revParam = req.getHeader("X-Hudson-Subversion-Revision");
}
long rev = -1;
if (revParam != null) {
rev = Long.parseLong(revParam);
}
boolean listenerDidSomething = false;
for (Listener listener : ExtensionList.lookup(Listener.class)) {
try {
if (listener.onNotify(uuid, rev, affectedPath)) {
listenerDidSomething = true;
}
} catch (Throwable t) {
LOGGER.log(WARNING, "Listener " + listener.getClass().getName() + " threw an uncaught exception", t);
}
}
if (!listenerDidSomething) LOGGER.log(Level.WARNING, "No interest in change to repository UUID {0} found", uuid);
rsp.setStatus(SC_OK);
}
private static class SubversionRepoUUIDAndRootPath {
public final UUID uuid;
public final String rootPath;
public SubversionRepoUUIDAndRootPath(UUID uuid, String rootPath) {
this.uuid = uuid;
this.rootPath = rootPath;
}
}
@Extension
public static class JobTriggerListenerImpl extends Listener {
private Map<String, UUID> remoteUUIDCache = new HashMap<>();
private JobProvider jobProvider = new JobProvider() {
@SuppressWarnings("rawtypes")
public List<Job> getAllJobs() {
return Jenkins.get().getAllItems(Job.class);
}
};
// for tests
void setJobProvider(JobProvider jobProvider) {
this.jobProvider = jobProvider;
}
private SubversionRepoUUIDAndRootPath remoteUUIDAndRootPathFromCacheOrFromSVN(Job job, SCM scm, ModuleLocation moduleLocation, String urlFromConfiguration) throws SVNException {
SubversionRepoUUIDAndRootPath uuidAndRootPath = null;
for (Map.Entry<String, UUID> e : remoteUUIDCache.entrySet()) {
String remoteRepoRootURL = e.getKey();
String remoteRepoRootURLWithSlash = remoteRepoRootURL + "/";
if (urlFromConfiguration.startsWith(remoteRepoRootURLWithSlash) || urlFromConfiguration.equals(remoteRepoRootURL)) {
UUID uuid = e.getValue();
String rootPath = SVNURL.parseURIDecoded(e.getKey()).getPath();
uuidAndRootPath = new SubversionRepoUUIDAndRootPath(uuid, rootPath);
LOGGER.finer("Using cached uuid for module location " + urlFromConfiguration + " of job "+ job);
break;
}
}
if (uuidAndRootPath == null) {
if (LOGGER.isLoggable(FINER)) {
LOGGER.finer("Could not find " + urlFromConfiguration + " in " + remoteUUIDCache.keySet());
}
UUID remoteUUID = moduleLocation.getUUID(job, scm);
SVNURL repositoryRoot = moduleLocation.getRepositoryRoot(job, scm);
remoteUUIDCache.put(repositoryRoot.toString(), remoteUUID);
uuidAndRootPath = new SubversionRepoUUIDAndRootPath(remoteUUID, repositoryRoot.getPath());
}
return uuidAndRootPath;
}
boolean doModuleLocationHasAPathFromAffectedPath(String configuredRepoFullPath, String rootRepoPath, Set<String> affectedPath) {
boolean containsAnAffectedPath = false;
if (configuredRepoFullPath.startsWith(rootRepoPath)) {
String remainingRepoPath = configuredRepoFullPath.substring(rootRepoPath.length());
if (remainingRepoPath.startsWith("/")) remainingRepoPath=remainingRepoPath.substring(1);
String remainingRepoPathSlash = remainingRepoPath + '/';
for (String path : affectedPath) {
if (path.equals(remainingRepoPath) /*for files*/ ||
path.startsWith(remainingRepoPathSlash) /*for dirs*/ ||
remainingRepoPath.length() == 0 /*when someone is checking out the whole repo (that is, configuredRepoFullPath==rootRepoPath)*/) {
// this project is possibly changed. poll now.
// if any of the data we used was bogus, the trigger will not detect a change
containsAnAffectedPath = true;
break;
}
}
}
return containsAnAffectedPath;
}
private void scheduleImediatePollingOfJob(Job job, SCMTrigger trigger, List<SvnInfo> infos) {
LOGGER.fine("Scheduling the immediate polling of " + job);
final RevisionParameterAction[] actions;
if (infos.isEmpty()) {
actions = new RevisionParameterAction[0];
} else {
actions = new RevisionParameterAction[] { new RevisionParameterAction(infos) };
}
trigger.run(actions);
}
@Override
public boolean onNotify(UUID uuid, long rev, Set<String> affectedPath) {
boolean scmFound = false, triggerFound = false, uuidFound = false, pathFound = false;
LOGGER.fine("Starting subversion locations checks for all jobs");
for (Job p : this.jobProvider.getAllJobs()) {
SCMTriggerItem scmTriggerItem = SCMTriggerItem.SCMTriggerItems.asSCMTriggerItem(p);
if (scmTriggerItem == null) {
continue;
}
if (p instanceof AbstractProject && ((AbstractProject) p).isDisabled()) {
continue;
}
String jobName = p.getName();
SCMS: for (SCM scm : scmTriggerItem.getSCMs()) {
if (scm instanceof SubversionSCM) scmFound = true; else continue;
SCMTrigger trigger = scmTriggerItem.getSCMTrigger();
if (trigger!=null && !doesIgnorePostCommitHooks(trigger)) triggerFound = true; else continue;
SubversionSCM sscm = (SubversionSCM) scm;
List<SvnInfo> infos = new ArrayList<>();
try {
boolean projectMatches = false;
for (ModuleLocation loc : sscm.getProjectLocations(p)) {
String urlFromConfiguration = loc.getURL();
//LOGGER.log(WARNING, "Checking uuid for module location + " + loc + " of job "+ p + " (urlFromConfiguration : " + urlFromConfiguration + ")");
try {
SubversionRepoUUIDAndRootPath uuidAndRootPath = this.remoteUUIDAndRootPathFromCacheOrFromSVN(p, sscm, loc, urlFromConfiguration);
UUID remoteUUID = uuidAndRootPath.uuid;
if (remoteUUID.equals(uuid)) uuidFound = true; else continue;
String configuredRepoFullPath = loc.getSVNURL().getPath();
String rootRepoPath = uuidAndRootPath.rootPath;
if (this.doModuleLocationHasAPathFromAffectedPath(configuredRepoFullPath, rootRepoPath, affectedPath)) {
projectMatches = true;
pathFound = true;
}
if ( rev != -1 ) {
infos.add(new SvnInfo(loc.getURL(), rev));
}
} catch (SVNCancelException e) {
LOGGER.log(WARNING, "Failed to handle Subversion commit notification (was trying to access " + urlFromConfiguration + " of job " + jobName + "). If you are using svn:externals feature ensure that the credentials of the externals are added on the Additional Credentials field", e);
} catch (SVNException e) {
LOGGER.log(WARNING, "Failed to handle Subversion commit notification (was trying to access " + urlFromConfiguration + " of job " + jobName + ")", e);
}
if (projectMatches) {
this.scheduleImediatePollingOfJob(p, trigger, infos);
break SCMS;
}
}
} catch(IOException e) {
LOGGER.log(WARNING, "Failed to handle Subversion commit notification (getting module locations failed for job " + jobName + ")", e);
}
}
}
LOGGER.fine("Ended subversion locations checks for all jobs");
if (!scmFound) LOGGER.warning("No subversion jobs found");
else if (!triggerFound) LOGGER.warning("No subversion jobs using SCM polling or all jobs using SCM polling are ignoring post-commit hooks");
else if (!uuidFound) LOGGER.warning("No subversion jobs using repository: " + uuid);
else if (!pathFound) LOGGER.fine("No jobs found matching the modified files");
return scmFound;
}
}
private static boolean doesIgnorePostCommitHooks(SCMTrigger trigger) {
if (IS_IGNORE_POST_COMMIT_HOOKS_METHOD == null)
return false;
try {
return (Boolean)IS_IGNORE_POST_COMMIT_HOOKS_METHOD.invoke(trigger, (Object[])null);
} catch (Exception e) {
LOGGER.log(WARNING,"Failure when calling isIgnorePostCommitHooks",e);
return false;
}
}
static {
try {
IS_IGNORE_POST_COMMIT_HOOKS_METHOD = SCMTrigger.class.getMethod("isIgnorePostCommitHooks", (Class[])null);
} catch (Exception e) {
// we're running in an older Jenkins version which doesn't have this method
}
}
private static final Logger LOGGER = Logger.getLogger(SubversionRepositoryStatus.class.getName());
}