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