switch to ReentrantLock: this will provide ability to use tryLock
[teamcity/git-plugin.git] / git-server / src / jetbrains / buildServer / buildTriggers / vcs / git / GitCommitSupport.java
1 /*
2  * Copyright 2000-2018 JetBrains s.r.o.
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
17 package jetbrains.buildServer.buildTriggers.vcs.git;
18
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.util.io.FileUtil;
21 import jetbrains.buildServer.util.StringUtil;
22 import jetbrains.buildServer.vcs.*;
23 import org.eclipse.jgit.dircache.DirCache;
24 import org.eclipse.jgit.dircache.DirCacheBuilder;
25 import org.eclipse.jgit.dircache.DirCacheEntry;
26 import org.eclipse.jgit.lib.*;
27 import org.eclipse.jgit.lib.Constants;
28 import org.eclipse.jgit.revwalk.RevCommit;
29 import org.eclipse.jgit.revwalk.RevWalk;
30 import org.eclipse.jgit.transport.RefSpec;
31 import org.eclipse.jgit.transport.RemoteRefUpdate;
32 import org.eclipse.jgit.transport.Transport;
33 import org.eclipse.jgit.treewalk.TreeWalk;
34 import org.eclipse.jgit.util.io.EolCanonicalizingInputStream;
35 import org.jetbrains.annotations.NotNull;
36
37 import java.io.ByteArrayInputStream;
38 import java.io.ByteArrayOutputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.util.*;
42 import java.util.concurrent.locks.Lock;
43 import java.util.concurrent.locks.ReentrantLock;
44
45 import static java.util.Arrays.asList;
46
47 public class GitCommitSupport implements CommitSupport, GitServerExtension {
48
49   private static final Logger LOG = Logger.getInstance(GitCommitSupport.class.getName());
50
51   private final GitVcsSupport myVcs;
52   private final CommitLoader myCommitLoader;
53   private final RepositoryManager myRepositoryManager;
54   private final TransportFactory myTransportFactory;
55   private final ServerPluginConfig myPluginConfig;
56
57   public GitCommitSupport(@NotNull GitVcsSupport vcs,
58                           @NotNull CommitLoader commitLoader,
59                           @NotNull RepositoryManager repositoryManager,
60                           @NotNull TransportFactory transportFactory,
61                           @NotNull ServerPluginConfig pluginConfig) {
62     myVcs = vcs;
63     myCommitLoader = commitLoader;
64     myRepositoryManager = repositoryManager;
65     myTransportFactory = transportFactory;
66     myPluginConfig = pluginConfig;
67     myVcs.addExtension(this);
68   }
69
70   @NotNull
71   public CommitPatchBuilder getCommitPatchBuilder(@NotNull VcsRoot root) throws VcsException {
72     OperationContext context = myVcs.createContext(root, "commit");
73     Lock rmLock = myRepositoryManager.getRmLock(context.getGitRoot().getRepositoryDir()).readLock();
74     rmLock.lock();
75     Repository db = context.getRepository();
76     return new GitCommitPatchBuilder(myVcs, context, myCommitLoader, db, myRepositoryManager, myTransportFactory, myPluginConfig, rmLock);
77   }
78
79
80   private static class GitCommitPatchBuilder implements CommitPatchBuilder {
81     private final GitVcsSupport myVcs;
82     private final OperationContext myContext;
83     private final CommitLoader myCommitLoader;
84     private final Repository myDb;
85     private final ObjectInserter myObjectWriter;
86     private final Map<String, ObjectId> myObjectMap = new HashMap<String, ObjectId>();
87     private final Set<String> myDeletedDirs = new HashSet<String>();
88     private final RepositoryManager myRepositoryManager;
89     private final TransportFactory myTransportFactory;
90     private final ServerPluginConfig myPluginConfig;
91     private final Lock myRmLock;
92
93     private GitCommitPatchBuilder(@NotNull GitVcsSupport vcs,
94                                   @NotNull OperationContext context,
95                                   @NotNull CommitLoader commitLoader,
96                                   @NotNull Repository db,
97                                   @NotNull RepositoryManager repositoryManager,
98                                   @NotNull TransportFactory transportFactory,
99                                   @NotNull ServerPluginConfig pluginConfig,
100                                   @NotNull Lock rmLock) {
101       myVcs = vcs;
102       myContext = context;
103       myCommitLoader = commitLoader;
104       myDb = db;
105       myObjectWriter = db.newObjectInserter();
106       myRepositoryManager = repositoryManager;
107       myTransportFactory = transportFactory;
108       myPluginConfig = pluginConfig;
109       myRmLock = rmLock;
110     }
111
112     public void createFile(@NotNull String path, @NotNull InputStream content) throws VcsException {
113       try {
114         ByteArrayOutputStream bytes = new ByteArrayOutputStream();
115         FileUtil.copy(content, bytes);
116         //taken from WorkingTreeIterator
117         EolCanonicalizingInputStream eolStream = new EolCanonicalizingInputStream(new ByteArrayInputStream(bytes.toByteArray()), true, true);
118         long length;
119         try {
120           length = computeLength(eolStream);
121         } catch (EolCanonicalizingInputStream.IsBinaryException e) {
122           //binary file, insert as is:
123           myObjectMap.put(path, myObjectWriter.insert(Constants.OBJ_BLOB, bytes.toByteArray()));
124           return;
125         } finally {
126           eolStream.close();
127         }
128         eolStream = new EolCanonicalizingInputStream(new ByteArrayInputStream(bytes.toByteArray()), true, true);
129         myObjectMap.put(path, myObjectWriter.insert(Constants.OBJ_BLOB, length, eolStream));
130       } catch (IOException e) {
131         throw new VcsException("Error while inserting file content to repository, file: " + path, e);
132       }
133     }
134
135
136     private long computeLength(InputStream in) throws IOException {
137       // Since we only care about the length, use skip. The stream
138       // may be able to more efficiently wade through its data.
139       //
140       long length = 0;
141       for (;;) {
142         long n = in.skip(1 << 20);
143         if (n <= 0)
144           break;
145         length += n;
146       }
147       return length;
148     }
149
150     public void deleteFile(@NotNull String path) {
151       myObjectMap.put(path, ObjectId.zeroId());
152     }
153
154     @NotNull
155     public CommitResult commit(@NotNull CommitSettings commitSettings) throws VcsException {
156       try {
157         LOG.info("Committing change '" + commitSettings.getDescription() + "'");
158         GitVcsRoot gitRoot = myContext.getGitRoot();
159         RevCommit lastCommit = getLastCommit(gitRoot);
160         LOG.info("Parent commit " + lastCommit.name());
161         ObjectId treeId = createNewTree(lastCommit);
162         if (!ObjectId.zeroId().equals(lastCommit.getId()) && lastCommit.getTree().getId().equals(treeId))
163           return CommitResult.createRepositoryUpToDateResult(lastCommit.getId().name());
164
165         ObjectId commitId = createCommit(gitRoot, lastCommit, treeId, commitSettings.getUserName(), nonEmptyMessage(commitSettings));
166
167         ReentrantLock lock = myRepositoryManager.getWriteLock(gitRoot.getRepositoryDir());
168         lock.lock();
169         try {
170           final Transport tn = myTransportFactory.createTransport(myDb, gitRoot.getRepositoryPushURL(), gitRoot.getAuthSettings(),
171                                                                   myPluginConfig.getPushTimeoutSeconds());
172           try {
173             RemoteRefUpdate ru = new RemoteRefUpdate(myDb, null, commitId, GitUtils.expandRef(gitRoot.getRef()), false, null, lastCommit);
174             tn.push(NullProgressMonitor.INSTANCE, Collections.singletonList(ru));
175             switch (ru.getStatus()) {
176               case UP_TO_DATE:
177               case OK:
178                 LOG.info("Change '" + commitSettings.getDescription() + "' was successfully committed");
179                 return CommitResult.createSuccessResult(commitId.name());
180               default: {
181                 StringBuilder error = new StringBuilder();
182                 error.append("Push failed, status: ").append(ru.getStatus());
183                 if (ru.getMessage() != null)
184                   error.append(", message: ").append(ru.getMessage());
185                 throw new VcsException(error.toString());
186               }
187             }
188           } catch (IOException e) {
189             LOG.warn("Error while pushing a commit, root " + gitRoot + ", revision " + commitId + ", destination " + GitUtils.expandRef(gitRoot.getRef()), e);
190             throw e;
191           } finally {
192             tn.close();
193           }
194         } finally {
195           lock.unlock();
196         }
197       } catch (Exception e) {
198         throw myContext.wrapException(e);
199       }
200     }
201
202     @NotNull
203     private String nonEmptyMessage(@NotNull CommitSettings commitSettings) {
204       String msg = commitSettings.getDescription();
205       if (!StringUtil.isEmpty(msg))
206         return msg;
207       return "no comments";
208     }
209
210     private ObjectId createCommit(@NotNull GitVcsRoot gitRoot,
211                                   @NotNull RevCommit parentCommit,
212                                   @NotNull ObjectId treeId,
213                                   @NotNull String userName,
214                                   @NotNull String description) throws IOException, VcsException {
215       CommitBuilder commit = new CommitBuilder();
216       commit.setTreeId(treeId);
217       if (!ObjectId.zeroId().equals(parentCommit.getId()))
218         commit.setParentIds(parentCommit);
219       commit.setCommitter(gitRoot.getTagger(myDb));
220       switch (gitRoot.getUsernameStyle()) {
221         case EMAIL:
222           int idx = userName.indexOf("@");
223           if (idx != -1) {
224             commit.setAuthor(new PersonIdent(userName.substring(0, idx), userName));
225           } else {
226             commit.setAuthor(new PersonIdent(userName, userName));
227           }
228           break;
229         case NAME:
230           commit.setAuthor(new PersonIdent(userName, userName + "@TeamCity"));
231           break;
232         case USERID:
233           commit.setAuthor(new PersonIdent(userName, userName + "@TeamCity"));
234           break;
235         case FULL:
236           commit.setAuthor(gitRoot.parseIdent(userName));
237           break;
238       }
239       commit.setMessage(description);
240       ObjectId commitId = myObjectWriter.insert(commit);
241       myObjectWriter.flush();
242       return commitId;
243     }
244
245     @NotNull
246     private ObjectId createNewTree(@NotNull RevCommit lastCommit) throws IOException {
247       DirCache inCoreIndex = DirCache.newInCore();
248       DirCacheBuilder tempBuilder = inCoreIndex.builder();
249       for (Map.Entry<String, ObjectId> e : myObjectMap.entrySet()) {
250         if (!ObjectId.zeroId().equals(e.getValue())) {
251           DirCacheEntry dcEntry = new DirCacheEntry(e.getKey());
252           dcEntry.setObjectId(e.getValue());
253           dcEntry.setFileMode(FileMode.REGULAR_FILE);
254           tempBuilder.add(dcEntry);
255         }
256       }
257
258       TreeWalk treeWalk = new TreeWalk(myDb);
259       if (!ObjectId.zeroId().equals(lastCommit.getId())) {
260         treeWalk.addTree(lastCommit.getTree());
261         treeWalk.setRecursive(true);
262         while (treeWalk.next()) {
263           String path = treeWalk.getPathString();
264           ObjectId newObjectId = myObjectMap.get(path);
265           if (newObjectId != null)
266             continue;
267
268           boolean deleted = false;
269           for (String dir : myDeletedDirs) {
270             if (path.startsWith(dir)) {
271               deleted = true;
272               break;
273             }
274           }
275           if (deleted)
276             continue;
277
278           DirCacheEntry dcEntry = new DirCacheEntry(path);
279           dcEntry.setFileMode(treeWalk.getFileMode(0));
280           dcEntry.setObjectId(treeWalk.getObjectId(0));
281           tempBuilder.add(dcEntry);
282         }
283       }
284       tempBuilder.finish();
285       return inCoreIndex.writeTree(myObjectWriter);
286     }
287
288     @NotNull
289     private RevCommit getLastCommit(@NotNull GitVcsRoot gitRoot) throws VcsException, IOException {
290       Map<String, Ref> refs = myVcs.getRemoteRefs(gitRoot.getOriginalRoot());
291       Ref ref = refs.get(GitUtils.expandRef(gitRoot.getRef()));
292       if (!refs.isEmpty() && ref == null)
293         throw new VcsException("The '" + gitRoot.getRef() + "' destination branch doesn't exist");
294       RevWalk revWalk = new RevWalk(myDb);
295       try {
296         if (ref == null)
297           return revWalk.lookupCommit(ObjectId.zeroId());
298         return revWalk.parseCommit(ref.getObjectId());
299       } catch (Exception e) {
300         //will try to fetch
301       } finally {
302         revWalk.release();
303       }
304       RefSpec spec = new RefSpec().setSource(GitUtils.expandRef(gitRoot.getRef()))
305         .setDestination(GitUtils.expandRef(gitRoot.getRef()))
306         .setForceUpdate(true);
307       myCommitLoader.fetch(myDb, gitRoot.getRepositoryFetchURL(), asList(spec), new FetchSettings(gitRoot.getAuthSettings()));
308       Ref defaultBranch = myDb.getRef(GitUtils.expandRef(gitRoot.getRef()));
309       return myCommitLoader.loadCommit(myContext, gitRoot, defaultBranch.getObjectId().name());
310     }
311
312     public void deleteDirectory(@NotNull final String path) {
313       String dirPath = path.endsWith("/") ? path : path + "/";
314       myDeletedDirs.add(dirPath);
315     }
316
317     public void createDirectory(@NotNull final String path) {
318     }
319
320     public void renameFile(@NotNull final String oldPath, @NotNull final String newPath, @NotNull InputStream content) throws VcsException {
321       deleteFile(oldPath);
322       createFile(newPath, content);
323     }
324
325     public void dispose() {
326       myRmLock.unlock();
327       myContext.close();
328     }
329   }
330 }