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