-
-
Notifications
You must be signed in to change notification settings - Fork 118
Expand file tree
/
Copy pathScmSyncConfigurationBusiness.java
More file actions
392 lines (337 loc) · 17.1 KB
/
ScmSyncConfigurationBusiness.java
File metadata and controls
392 lines (337 loc) · 17.1 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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
package hudson.plugins.scm_sync_configuration;
import com.google.common.io.Files;
import hudson.model.User;
import hudson.plugins.scm_sync_configuration.exceptions.LoggableException;
import hudson.plugins.scm_sync_configuration.model.*;
import hudson.plugins.scm_sync_configuration.strategies.ScmSyncStrategy;
import hudson.plugins.scm_sync_configuration.utils.Checksums;
import hudson.security.Permission;
import hudson.util.DaemonThreadFactory;
import jenkins.model.Jenkins;
import org.apache.commons.io.FileUtils;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.manager.ScmManager;
import org.codehaus.plexus.PlexusContainerException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Logger;
public class ScmSyncConfigurationBusiness {
private static final String WORKING_DIRECTORY_PATH = "/scm-sync-configuration/";
private static final String CHECKOUT_SCM_DIRECTORY = "checkoutConfiguration";
private static final Logger LOGGER = Logger.getLogger(ScmSyncConfigurationBusiness.class.getName());
private boolean checkoutSucceeded;
private SCMManipulator scmManipulator;
private File checkoutScmDirectory = null;
private ScmSyncConfigurationStatusManager scmSyncConfigurationStatusManager = null;
/**
* Use of a size 1 thread pool frees us from worrying about accidental thread death and
* changeset commit concurrency
*/
/*package*/ final ExecutorService writer = Executors.newFixedThreadPool(1, new DaemonThreadFactory());
// TODO: Refactor this into the plugin object ???
private List<Commit> commitsQueue = Collections.synchronizedList(new ArrayList<Commit>());
public ScmSyncConfigurationBusiness(){
}
public ScmSyncConfigurationStatusManager getScmSyncConfigurationStatusManager() {
if (scmSyncConfigurationStatusManager == null) {
scmSyncConfigurationStatusManager = new ScmSyncConfigurationStatusManager();
}
return scmSyncConfigurationStatusManager;
}
public void init(ScmContext scmContext) throws ComponentLookupException, PlexusContainerException {
ScmManager scmManager = SCMManagerFactory.getInstance().createScmManager();
this.scmManipulator = new SCMManipulator(scmManager);
this.checkoutScmDirectory = new File(getCheckoutScmDirectoryAbsolutePath());
this.checkoutSucceeded = false;
initializeRepository(scmContext, true);
}
public void initializeRepository(ScmContext scmContext, boolean deleteCheckoutScmDir){
// Let's check if everything is available to checkout sources
if(scmManipulator != null && scmManipulator.scmConfigurationSettledUp(scmContext, true)){
LOGGER.info("Initializing SCM repository for scm-sync-configuration plugin ...");
// If checkoutScmDirectory was not empty and deleteCheckoutScmDir is asked, reinitialize it !
if(deleteCheckoutScmDir){
cleanChekoutScmDirectory();
}
// Creating checkout scm directory
if(!checkoutScmDirectory.exists()){
try {
FileUtils.forceMkdir(checkoutScmDirectory);
LOGGER.info("Directory ["+ checkoutScmDirectory.getAbsolutePath() +"] created !");
} catch (IOException e) {
LOGGER.warning("Directory ["+ checkoutScmDirectory.getAbsolutePath() +"] cannot be created !");
}
}
this.checkoutSucceeded = this.scmManipulator.checkout(this.checkoutScmDirectory);
if(this.checkoutSucceeded){
LOGGER.info("SCM repository initialization done.");
}
signal("Checkout " + this.checkoutScmDirectory, this.checkoutSucceeded);
}
}
public void cleanChekoutScmDirectory(){
if(checkoutScmDirectory != null && checkoutScmDirectory.exists()){
LOGGER.info("Deleting old checkout SCM directory ...");
try {
FileUtils.forceDelete(checkoutScmDirectory);
} catch (IOException e) {
LOGGER.throwing(FileUtils.class.getName(), "forceDelete", e);
LOGGER.severe("Error while deleting ["+checkoutScmDirectory.getAbsolutePath()+"] : "+e.getMessage());
}
this.checkoutSucceeded = false;
}
}
public List<File> deleteHierarchy(ScmContext scmContext, Path hierarchyPath){
if(scmManipulator == null || !scmManipulator.scmConfigurationSettledUp(scmContext, false)){
return null;
}
File rootHierarchyTranslatedInScm = hierarchyPath.getScmFile();
List<File> filesToCommit = scmManipulator.deleteHierarchy(rootHierarchyTranslatedInScm);
// Once done, we should delete path in scm if it is a directory
if(hierarchyPath.isDirectory()){
try {
FileUtils.deleteDirectory(rootHierarchyTranslatedInScm);
} catch (IOException e) {
throw new LoggableException("Failed to recursively delete scm directory "+rootHierarchyTranslatedInScm.getAbsolutePath(), FileUtils.class, "deleteDirectory", e);
}
}
signal("Delete " + hierarchyPath, filesToCommit != null);
return filesToCommit;
}
public Future<Void> queueChangeSet(final ScmContext scmContext, ChangeSet changeset, User user, String userMessage) {
if(scmManipulator == null || !scmManipulator.scmConfigurationSettledUp(scmContext, false)){
LOGGER.info("Queue of changeset "+changeset.toString()+" aborted (scm manipulator not settled !)");
return null;
}
Commit commit = new Commit(changeset, user, userMessage, scmContext);
LOGGER.finest("Queuing commit "+commit.toString()+" to SCM ...");
commitsQueue.add(commit);
return writer.submit(new Callable<Void>() {
public Void call() throws Exception {
processCommitsQueue();
return null;
}
});
}
private void processCommitsQueue() {
File scmRoot = new File(getCheckoutScmDirectoryAbsolutePath());
// Copying shared commitQueue in order to allow conccurrent modification
List<Commit> currentCommitQueue = new ArrayList<Commit>(commitsQueue);
List<Commit> checkedInCommits = new ArrayList<Commit>();
try {
// Reading commit queue and commiting changeset
for(Commit commit: currentCommitQueue){
String logMessage = "Processing commit : " + commit.toString();
LOGGER.finest(logMessage);
// Preparing files to add / delete
List<File> updatedFiles = new ArrayList<File>();
for(Map.Entry<Path,byte[]> pathContent : commit.getChangeset().getPathContents().entrySet()){
Path pathRelativeToJenkinsRoot = pathContent.getKey();
byte[] content = pathContent.getValue();
File fileTranslatedInScm = pathRelativeToJenkinsRoot.getScmFile();
if(pathRelativeToJenkinsRoot.isDirectory()) {
if(!fileTranslatedInScm.exists()){
// Retrieving non existing parent scm path *before* copying it from jenkins directory
String firstNonExistingParentScmPath = pathRelativeToJenkinsRoot.getFirstNonExistingParentScmPath();
try {
FileUtils.copyDirectory(JenkinsFilesHelper.buildFileFromPathRelativeToHudsonRoot(pathRelativeToJenkinsRoot.getPath()),
fileTranslatedInScm);
} catch (IOException e) {
throw new LoggableException("Error while copying file hierarchy to SCM checkouted directory", FileUtils.class, "copyDirectory", e);
}
updatedFiles.addAll(scmManipulator.addFile(scmRoot, firstNonExistingParentScmPath));
}
} else {
// We should remember if file in scm existed or not before any manipulation,
// especially writing content
boolean fileTranslatedInScmInitiallyExists = fileTranslatedInScm.exists();
boolean fileContentModified = writeScmContentOnlyIfItDiffers(pathRelativeToJenkinsRoot, content, fileTranslatedInScm);
if(fileTranslatedInScmInitiallyExists){
if(fileContentModified){
// No need to call scmManipulator.addFile() if fileTranslatedInScm already existed
updatedFiles.add(fileTranslatedInScm);
}
} else {
updatedFiles.addAll(scmManipulator.addFile(scmRoot, pathRelativeToJenkinsRoot.getPath()));
}
}
}
for(Path path : commit.getChangeset().getPathsToDelete()){
List<File> deletedFiles = deleteHierarchy(commit.getScmContext(), path);
updatedFiles.addAll(deletedFiles);
}
if(updatedFiles.isEmpty()){
LOGGER.finest("Empty changeset to commit (no changes found on files) => commit skipped !");
checkedInCommits.add(commit);
} else {
// Commiting files...
boolean result = scmManipulator.checkinFiles(scmRoot, commit.getMessage());
if(result){
LOGGER.finest("Commit "+commit.toString()+" pushed to SCM !");
checkedInCommits.add(commit);
} else {
throw new LoggableException("Error while checking in file to scm repository", SCMManipulator.class, "checkinFiles");
}
signal(logMessage, true);
}
}
// As soon as a commit doesn't goes well, we should abort commit queue processing...
}catch(LoggableException e){
LOGGER.throwing(e.getClazz().getName(), e.getMethodName(), e);
LOGGER.severe("Error while processing commit queue : "+e.getMessage());
signal(e.getMessage(), false);
} finally {
// We should remove every checkedInCommits
commitsQueue.removeAll(checkedInCommits);
}
}
private boolean writeScmContentOnlyIfItDiffers(Path pathRelativeToJenkinsRoot, byte[] content, File fileTranslatedInScm)
throws LoggableException {
boolean scmContentUpdated = false;
boolean contentDiffer = false;
try {
contentDiffer = !Checksums.fileAndByteArrayContentAreEqual(fileTranslatedInScm, content);
} catch (IOException e) {
throw new LoggableException("Error while checking content checksum", Checksums.class, "fileAndByteArrayContentAreEqual", e);
}
if(contentDiffer){
createScmContent(pathRelativeToJenkinsRoot, content, fileTranslatedInScm);
scmContentUpdated = true;
} else {
// Don't do anything
}
return scmContentUpdated;
}
private void createScmContent(Path pathRelativeToJenkinsRoot, byte[] content, File fileTranslatedInScm)
throws LoggableException {
Stack<File> directoriesToCreate = new Stack<File>();
File directory = fileTranslatedInScm.getParentFile();
// Eventually, creating non existing enclosing directories
while(!directory.exists()){
directoriesToCreate.push(directory);
directory = directory.getParentFile();
}
while(!directoriesToCreate.empty()){
directory = directoriesToCreate.pop();
if(!directory.mkdir()){
throw new LoggableException("Error while creating directory "+directory.getAbsolutePath(), File.class, "mkdir");
}
}
try {
// Copying content if pathRelativeToJenkinsRoot is a file, or creating the directory if it is a directory
if(pathRelativeToJenkinsRoot.isDirectory()){
if(!fileTranslatedInScm.mkdir()){
throw new LoggableException("Error while creating directory "+fileTranslatedInScm.getAbsolutePath(), File.class, "mkdir");
}
} else {
Files.write(content, fileTranslatedInScm);
}
} catch (IOException e) {
throw new LoggableException("Error while creating file in checkouted directory", Files.class, "write", e);
}
}
public void synchronizeAllConfigs(ScmSyncStrategy[] availableStrategies){
List<File> filesToSync = new ArrayList<File>();
// Building synced files from strategies
for(ScmSyncStrategy strategy : availableStrategies){
filesToSync.addAll(strategy.createInitializationSynchronizedFileset());
}
ScmSyncConfigurationPlugin plugin = ScmSyncConfigurationPlugin.getInstance();
plugin.startThreadedTransaction();
try {
for(File fileToSync : filesToSync){
String hudsonConfigPathRelativeToHudsonRoot = JenkinsFilesHelper.buildPathRelativeToHudsonRoot(fileToSync);
plugin.getTransaction().defineCommitMessage(new WeightedMessage("Repository initialization", MessageWeight.IMPORTANT));
plugin.getTransaction().registerPath(hudsonConfigPathRelativeToHudsonRoot);
}
} finally {
plugin.getTransaction().commit();
}
}
public boolean scmCheckoutDirectorySettledUp(ScmContext scmContext){
return scmManipulator != null && this.scmManipulator.scmConfigurationSettledUp(scmContext, false) && this.checkoutSucceeded;
}
public List<File> reloadAllFilesFromScm() throws IOException, ScmException {
//removeSourceJobsDuringReload();
this.scmManipulator.update(new File(getCheckoutScmDirectoryAbsolutePath()));
return syncDirectories(new File(getCheckoutScmDirectoryAbsolutePath() + File.separator), "");
}
/**
*
* @return
* @throws IOException
*/
public List<File> removeSourceJobsDuringReload() throws IOException {
List<File> l = new ArrayList<File>();
File jobsFolder = new File(Jenkins.getInstance().getRootDir().toString()+ "/jobs");
LOGGER.info("Configuration reload - jobs removal. Cleaning up folder:"+jobsFolder);
for(File f : jobsFolder.listFiles()){
l.add(f);
if (f.isDirectory()) {
FileUtils.deleteDirectory(f);
} else {
FileUtils.forceDelete(f);
}
}
return l;
}
private List<File> syncDirectories(File from, String relative) throws IOException {
List<File> l = new ArrayList<File>();
for(File f : from.listFiles()) {
String newRelative = relative + File.separator + f.getName();
File jenkinsFile = new File(Jenkins.getInstance().getRootDir() + newRelative);
if (f.getName().equals(scmManipulator.getScmSpecificFilename())) {
// nothing to do
}
else if (f.isDirectory()) {
if (!jenkinsFile.exists()) {
FileUtils.copyDirectory(f, jenkinsFile, new FileFilter() {
public boolean accept(File f) {
return !f.getName().equals(scmManipulator.getScmSpecificFilename());
}
});
l.add(jenkinsFile);
}
else {
l.addAll(syncDirectories(f, newRelative));
}
}
else {
if (!jenkinsFile.exists() || !FileUtils.contentEquals(f, jenkinsFile)) {
FileUtils.copyFile(f, jenkinsFile);
l.add(jenkinsFile);
}
}
}
return l;
}
private void signal(String operation, boolean result) {
if (result) {
getScmSyncConfigurationStatusManager().signalSuccess();
}
else {
getScmSyncConfigurationStatusManager().signalFailed(operation);
}
}
public static String getCheckoutScmDirectoryAbsolutePath(){
return Jenkins.getInstance().getRootDir().getAbsolutePath()+WORKING_DIRECTORY_PATH+CHECKOUT_SCM_DIRECTORY;
}
public void purgeFailLogs() {
Jenkins.getInstance().checkPermission(purgeFailLogPermission());
scmSyncConfigurationStatusManager.purgeFailLogs();
}
public boolean canCurrentUserPurgeFailLogs() {
return Jenkins.getInstance().hasPermission(purgeFailLogPermission());
}
private static Permission purgeFailLogPermission(){
// Only administrators should be able to purge logs
return Jenkins.ADMINISTER;
}
}