View Javadoc
1   /*
2    * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
3    * Copyright (C) 2010, Stefan Lay <stefan.lay@sap.com>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  package org.eclipse.jgit.api;
45  
46  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
47  import static org.eclipse.jgit.lib.FileMode.GITLINK;
48  import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
49  import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
50  
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.time.Instant;
54  import java.util.Collection;
55  import java.util.LinkedList;
56  
57  import org.eclipse.jgit.api.errors.FilterFailedException;
58  import org.eclipse.jgit.api.errors.GitAPIException;
59  import org.eclipse.jgit.api.errors.JGitInternalException;
60  import org.eclipse.jgit.api.errors.NoFilepatternException;
61  import org.eclipse.jgit.dircache.DirCache;
62  import org.eclipse.jgit.dircache.DirCacheBuildIterator;
63  import org.eclipse.jgit.dircache.DirCacheBuilder;
64  import org.eclipse.jgit.dircache.DirCacheEntry;
65  import org.eclipse.jgit.dircache.DirCacheIterator;
66  import org.eclipse.jgit.internal.JGitText;
67  import org.eclipse.jgit.lib.FileMode;
68  import org.eclipse.jgit.lib.ObjectId;
69  import org.eclipse.jgit.lib.ObjectInserter;
70  import org.eclipse.jgit.lib.Repository;
71  import org.eclipse.jgit.treewalk.FileTreeIterator;
72  import org.eclipse.jgit.treewalk.NameConflictTreeWalk;
73  import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
74  import org.eclipse.jgit.treewalk.WorkingTreeIterator;
75  import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
76  
77  /**
78   * A class used to execute a {@code Add} command. It has setters for all
79   * supported options and arguments of this command and a {@link #call()} method
80   * to finally execute the command. Each instance of this class should only be
81   * used for one invocation of the command (means: one call to {@link #call()})
82   *
83   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-add.html"
84   *      >Git documentation about Add</a>
85   */
86  public class AddCommand extends GitCommand<DirCache> {
87  
88  	private Collection<String> filepatterns;
89  
90  	private WorkingTreeIterator workingTreeIterator;
91  
92  	private boolean update = false;
93  
94  	/**
95  	 * Constructor for AddCommand
96  	 *
97  	 * @param repo
98  	 *            the {@link org.eclipse.jgit.lib.Repository}
99  	 */
100 	public AddCommand(Repository repo) {
101 		super(repo);
102 		filepatterns = new LinkedList<>();
103 	}
104 
105 	/**
106 	 * Add a path to a file/directory whose content should be added.
107 	 * <p>
108 	 * A directory name (e.g. <code>dir</code> to add <code>dir/file1</code> and
109 	 * <code>dir/file2</code>) can also be given to add all files in the
110 	 * directory, recursively. Fileglobs (e.g. *.c) are not yet supported.
111 	 *
112 	 * @param filepattern
113 	 *            repository-relative path of file/directory to add (with
114 	 *            <code>/</code> as separator)
115 	 * @return {@code this}
116 	 */
117 	public AddCommand addFilepattern(String filepattern) {
118 		checkCallable();
119 		filepatterns.add(filepattern);
120 		return this;
121 	}
122 
123 	/**
124 	 * Allow clients to provide their own implementation of a FileTreeIterator
125 	 *
126 	 * @param f
127 	 *            a {@link org.eclipse.jgit.treewalk.WorkingTreeIterator}
128 	 *            object.
129 	 * @return {@code this}
130 	 */
131 	public AddCommand setWorkingTreeIterator(WorkingTreeIterator f) {
132 		workingTreeIterator = f;
133 		return this;
134 	}
135 
136 	/**
137 	 * {@inheritDoc}
138 	 * <p>
139 	 * Executes the {@code Add} command. Each instance of this class should only
140 	 * be used for one invocation of the command. Don't call this method twice
141 	 * on an instance.
142 	 */
143 	@Override
144 	public DirCache call() throws GitAPIException, NoFilepatternException {
145 
146 		if (filepatterns.isEmpty())
147 			throw new NoFilepatternException(JGitText.get().atLeastOnePatternIsRequired);
148 		checkCallable();
149 		DirCache dc = null;
150 		boolean addAll = filepatterns.contains("."); //$NON-NLS-1$
151 
152 		try (ObjectInserter inserter = repo.newObjectInserter();
153 				NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) {
154 			tw.setOperationType(OperationType.CHECKIN_OP);
155 			dc = repo.lockDirCache();
156 
157 			DirCacheBuilder builder = dc.builder();
158 			tw.addTree(new DirCacheBuildIterator(builder));
159 			if (workingTreeIterator == null)
160 				workingTreeIterator = new FileTreeIterator(repo);
161 			workingTreeIterator.setDirCacheIterator(tw, 0);
162 			tw.addTree(workingTreeIterator);
163 			if (!addAll)
164 				tw.setFilter(PathFilterGroup.createFromStrings(filepatterns));
165 
166 			byte[] lastAdded = null;
167 
168 			while (tw.next()) {
169 				DirCacheIterator c = tw.getTree(0, DirCacheIterator.class);
170 				WorkingTreeIterator f = tw.getTree(1, WorkingTreeIterator.class);
171 				if (c == null && f != null && f.isEntryIgnored()) {
172 					// file is not in index but is ignored, do nothing
173 					continue;
174 				} else if (c == null && update) {
175 					// Only update of existing entries was requested.
176 					continue;
177 				}
178 
179 				DirCacheEntry entry = c != null ? c.getDirCacheEntry() : null;
180 				if (entry != null && entry.getStage() > 0
181 						&& lastAdded != null
182 						&& lastAdded.length == tw.getPathLength()
183 						&& tw.isPathPrefix(lastAdded, lastAdded.length) == 0) {
184 					// In case of an existing merge conflict the
185 					// DirCacheBuildIterator iterates over all stages of
186 					// this path, we however want to add only one
187 					// new DirCacheEntry per path.
188 					continue;
189 				}
190 
191 				if (tw.isSubtree() && !tw.isDirectoryFileConflict()) {
192 					tw.enterSubtree();
193 					continue;
194 				}
195 
196 				if (f == null) { // working tree file does not exist
197 					if (entry != null
198 							&& (!update || GITLINK == entry.getFileMode())) {
199 						builder.add(entry);
200 					}
201 					continue;
202 				}
203 
204 				if (entry != null && entry.isAssumeValid()) {
205 					// Index entry is marked assume valid. Even though
206 					// the user specified the file to be added JGit does
207 					// not consider the file for addition.
208 					builder.add(entry);
209 					continue;
210 				}
211 
212 				if ((f.getEntryRawMode() == TYPE_TREE
213 						&& f.getIndexFileMode(c) != FileMode.GITLINK) ||
214 						(f.getEntryRawMode() == TYPE_GITLINK
215 								&& f.getIndexFileMode(c) == FileMode.TREE)) {
216 					// Index entry exists and is symlink, gitlink or file,
217 					// otherwise the tree would have been entered above.
218 					// Replace the index entry by diving into tree of files.
219 					tw.enterSubtree();
220 					continue;
221 				}
222 
223 				byte[] path = tw.getRawPath();
224 				if (entry == null || entry.getStage() > 0) {
225 					entry = new DirCacheEntry(path);
226 				}
227 				FileMode mode = f.getIndexFileMode(c);
228 				entry.setFileMode(mode);
229 
230 				if (GITLINK != mode) {
231 					entry.setLength(f.getEntryLength());
232 					entry.setLastModified(f.getEntryLastModifiedInstant());
233 					long len = f.getEntryContentLength();
234 					// We read and filter the content multiple times.
235 					// f.getEntryContentLength() reads and filters the input and
236 					// inserter.insert(...) does it again. That's because an
237 					// ObjectInserter needs to know the length before it starts
238 					// inserting. TODO: Fix this by using Buffers.
239 					try (InputStream in = f.openEntryStream()) {
240 						ObjectId id = inserter.insert(OBJ_BLOB, len, in);
241 						entry.setObjectId(id);
242 					}
243 				} else {
244 					entry.setLength(0);
245 					entry.setLastModified(Instant.ofEpochSecond(0));
246 					entry.setObjectId(f.getEntryObjectId());
247 				}
248 				builder.add(entry);
249 				lastAdded = path;
250 			}
251 			inserter.flush();
252 			builder.commit();
253 			setCallable(false);
254 		} catch (IOException e) {
255 			Throwable cause = e.getCause();
256 			if (cause != null && cause instanceof FilterFailedException)
257 				throw (FilterFailedException) cause;
258 			throw new JGitInternalException(
259 					JGitText.get().exceptionCaughtDuringExecutionOfAddCommand, e);
260 		} finally {
261 			if (dc != null)
262 				dc.unlock();
263 		}
264 
265 		return dc;
266 	}
267 
268 	/**
269 	 * Set whether to only match against already tracked files
270 	 *
271 	 * @param update
272 	 *            If set to true, the command only matches {@code filepattern}
273 	 *            against already tracked files in the index rather than the
274 	 *            working tree. That means that it will never stage new files,
275 	 *            but that it will stage modified new contents of tracked files
276 	 *            and that it will remove files from the index if the
277 	 *            corresponding files in the working tree have been removed. In
278 	 *            contrast to the git command line a {@code filepattern} must
279 	 *            exist also if update is set to true as there is no concept of
280 	 *            a working directory here.
281 	 * @return {@code this}
282 	 */
283 	public AddCommand setUpdate(boolean update) {
284 		this.update = update;
285 		return this;
286 	}
287 
288 	/**
289 	 * Whether to only match against already tracked files
290 	 *
291 	 * @return whether to only match against already tracked files
292 	 */
293 	public boolean isUpdate() {
294 		return update;
295 	}
296 }