View Javadoc
1   /*
2    * Copyright (C) 2011, 2020 IBM Corporation and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.api;
11  
12  import java.io.File;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.io.Writer;
16  import java.nio.file.Files;
17  import java.nio.file.StandardCopyOption;
18  import java.text.MessageFormat;
19  import java.util.ArrayList;
20  import java.util.Iterator;
21  import java.util.List;
22  
23  import org.eclipse.jgit.api.errors.GitAPIException;
24  import org.eclipse.jgit.api.errors.PatchApplyException;
25  import org.eclipse.jgit.api.errors.PatchFormatException;
26  import org.eclipse.jgit.diff.DiffEntry.ChangeType;
27  import org.eclipse.jgit.diff.RawText;
28  import org.eclipse.jgit.internal.JGitText;
29  import org.eclipse.jgit.lib.FileMode;
30  import org.eclipse.jgit.lib.Repository;
31  import org.eclipse.jgit.patch.FileHeader;
32  import org.eclipse.jgit.patch.HunkHeader;
33  import org.eclipse.jgit.patch.Patch;
34  import org.eclipse.jgit.util.FileUtils;
35  
36  /**
37   * Apply a patch to files and/or to the index.
38   *
39   * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-apply.html"
40   *      >Git documentation about apply</a>
41   * @since 2.0
42   */
43  public class ApplyCommand extends GitCommand<ApplyResult> {
44  
45  	private InputStream in;
46  
47  	/**
48  	 * Constructs the command if the patch is to be applied to the index.
49  	 *
50  	 * @param repo
51  	 */
52  	ApplyCommand(Repository repo) {
53  		super(repo);
54  	}
55  
56  	/**
57  	 * Set patch
58  	 *
59  	 * @param in
60  	 *            the patch to apply
61  	 * @return this instance
62  	 */
63  	public ApplyCommand setPatch(InputStream in) {
64  		checkCallable();
65  		this.in = in;
66  		return this;
67  	}
68  
69  	/**
70  	 * {@inheritDoc}
71  	 * <p>
72  	 * Executes the {@code ApplyCommand} command with all the options and
73  	 * parameters collected by the setter methods (e.g.
74  	 * {@link #setPatch(InputStream)} of this class. Each instance of this class
75  	 * should only be used for one invocation of the command. Don't call this
76  	 * method twice on an instance.
77  	 */
78  	@Override
79  	public ApplyResult call() throws GitAPIException, PatchFormatException,
80  			PatchApplyException {
81  		checkCallable();
82  		ApplyResult r = new ApplyResult();
83  		try {
84  			final Patch/Patch.html#Patch">Patch p = new Patch();
85  			try {
86  				p.parse(in);
87  			} finally {
88  				in.close();
89  			}
90  			if (!p.getErrors().isEmpty())
91  				throw new PatchFormatException(p.getErrors());
92  			for (FileHeader fh : p.getFiles()) {
93  				ChangeType type = fh.getChangeType();
94  				File f = null;
95  				switch (type) {
96  				case ADD:
97  					f = getFile(fh.getNewPath(), true);
98  					apply(f, fh);
99  					break;
100 				case MODIFY:
101 					f = getFile(fh.getOldPath(), false);
102 					apply(f, fh);
103 					break;
104 				case DELETE:
105 					f = getFile(fh.getOldPath(), false);
106 					if (!f.delete())
107 						throw new PatchApplyException(MessageFormat.format(
108 								JGitText.get().cannotDeleteFile, f));
109 					break;
110 				case RENAME:
111 					f = getFile(fh.getOldPath(), false);
112 					File dest = getFile(fh.getNewPath(), false);
113 					try {
114 						FileUtils.mkdirs(dest.getParentFile(), true);
115 						FileUtils.rename(f, dest,
116 								StandardCopyOption.ATOMIC_MOVE);
117 					} catch (IOException e) {
118 						throw new PatchApplyException(MessageFormat.format(
119 								JGitText.get().renameFileFailed, f, dest), e);
120 					}
121 					apply(dest, fh);
122 					break;
123 				case COPY:
124 					f = getFile(fh.getOldPath(), false);
125 					File target = getFile(fh.getNewPath(), false);
126 					FileUtils.mkdirs(target.getParentFile(), true);
127 					Files.copy(f.toPath(), target.toPath());
128 					apply(target, fh);
129 				}
130 				r.addUpdatedFile(f);
131 			}
132 		} catch (IOException e) {
133 			throw new PatchApplyException(MessageFormat.format(
134 					JGitText.get().patchApplyException, e.getMessage()), e);
135 		}
136 		setCallable(false);
137 		return r;
138 	}
139 
140 	private File getFile(String path, boolean create)
141 			throws PatchApplyException {
142 		File f = new File(getRepository().getWorkTree(), path);
143 		if (create)
144 			try {
145 				File parent = f.getParentFile();
146 				FileUtils.mkdirs(parent, true);
147 				FileUtils.createNewFile(f);
148 			} catch (IOException e) {
149 				throw new PatchApplyException(MessageFormat.format(
150 						JGitText.get().createNewFileFailed, f), e);
151 			}
152 		return f;
153 	}
154 
155 	/**
156 	 * @param f
157 	 * @param fh
158 	 * @throws IOException
159 	 * @throws PatchApplyException
160 	 */
161 	private void apply(File f, FileHeader fh)
162 			throws IOException, PatchApplyException {
163 		RawText rt = new RawText(f);
164 		List<String> oldLines = new ArrayList<>(rt.size());
165 		for (int i = 0; i < rt.size(); i++)
166 			oldLines.add(rt.getString(i));
167 		List<String> newLines = new ArrayList<>(oldLines);
168 		int afterLastHunk = 0;
169 		int lineNumberShift = 0;
170 		int lastHunkNewLine = -1;
171 		for (HunkHeader hh : fh.getHunks()) {
172 
173 			// We assume hunks to be ordered
174 			if (hh.getNewStartLine() <= lastHunkNewLine) {
175 				throw new PatchApplyException(MessageFormat
176 						.format(JGitText.get().patchApplyException, hh));
177 			}
178 			lastHunkNewLine = hh.getNewStartLine();
179 
180 			byte[] b = new byte[hh.getEndOffset() - hh.getStartOffset()];
181 			System.arraycopy(hh.getBuffer(), hh.getStartOffset(), b, 0,
182 					b.length);
183 			RawText hrt = new RawText(b);
184 
185 			List<String> hunkLines = new ArrayList<>(hrt.size());
186 			for (int i = 0; i < hrt.size(); i++) {
187 				hunkLines.add(hrt.getString(i));
188 			}
189 
190 			if (hh.getNewStartLine() == 0) {
191 				// Must be the single hunk for clearing all content
192 				if (fh.getHunks().size() == 1
193 						&& canApplyAt(hunkLines, newLines, 0)) {
194 					newLines.clear();
195 					break;
196 				}
197 				throw new PatchApplyException(MessageFormat
198 						.format(JGitText.get().patchApplyException, hh));
199 			}
200 			// Hunk lines as reported by the hunk may be off, so don't rely on
201 			// them.
202 			int applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
203 			// But they definitely should not go backwards.
204 			if (applyAt < afterLastHunk && lineNumberShift < 0) {
205 				applyAt = hh.getNewStartLine() - 1;
206 				lineNumberShift = 0;
207 			}
208 			if (applyAt < afterLastHunk) {
209 				throw new PatchApplyException(MessageFormat
210 						.format(JGitText.get().patchApplyException, hh));
211 			}
212 			boolean applies = false;
213 			int oldLinesInHunk = hh.getLinesContext()
214 					+ hh.getOldImage().getLinesDeleted();
215 			if (oldLinesInHunk <= 1) {
216 				// Don't shift hunks without context lines. Just try the
217 				// position corrected by the current lineNumberShift, and if
218 				// that fails, the position recorded in the hunk header.
219 				applies = canApplyAt(hunkLines, newLines, applyAt);
220 				if (!applies && lineNumberShift != 0) {
221 					applyAt = hh.getNewStartLine() - 1;
222 					applies = applyAt >= afterLastHunk
223 							&& canApplyAt(hunkLines, newLines, applyAt);
224 				}
225 			} else {
226 				int maxShift = applyAt - afterLastHunk;
227 				for (int shift = 0; shift <= maxShift; shift++) {
228 					if (canApplyAt(hunkLines, newLines, applyAt - shift)) {
229 						applies = true;
230 						applyAt -= shift;
231 						break;
232 					}
233 				}
234 				if (!applies) {
235 					// Try shifting the hunk downwards
236 					applyAt = hh.getNewStartLine() - 1 + lineNumberShift;
237 					maxShift = newLines.size() - applyAt - oldLinesInHunk;
238 					for (int shift = 1; shift <= maxShift; shift++) {
239 						if (canApplyAt(hunkLines, newLines, applyAt + shift)) {
240 							applies = true;
241 							applyAt += shift;
242 							break;
243 						}
244 					}
245 				}
246 			}
247 			if (!applies) {
248 				throw new PatchApplyException(MessageFormat
249 						.format(JGitText.get().patchApplyException, hh));
250 			}
251 			// Hunk applies at applyAt. Apply it, and update afterLastHunk and
252 			// lineNumberShift
253 			lineNumberShift = applyAt - hh.getNewStartLine() + 1;
254 			int sz = hunkLines.size();
255 			for (int j = 1; j < sz; j++) {
256 				String hunkLine = hunkLines.get(j);
257 				switch (hunkLine.charAt(0)) {
258 				case ' ':
259 					applyAt++;
260 					break;
261 				case '-':
262 					newLines.remove(applyAt);
263 					break;
264 				case '+':
265 					newLines.add(applyAt++, hunkLine.substring(1));
266 					break;
267 				default:
268 					break;
269 				}
270 			}
271 			afterLastHunk = applyAt;
272 		}
273 		if (!isNoNewlineAtEndOfFile(fh)) {
274 			newLines.add(""); //$NON-NLS-1$
275 		}
276 		if (!rt.isMissingNewlineAtEnd()) {
277 			oldLines.add(""); //$NON-NLS-1$
278 		}
279 		if (!isChanged(oldLines, newLines)) {
280 			return; // Don't touch the file
281 		}
282 		try (Writer fw = Files.newBufferedWriter(f.toPath())) {
283 			for (Iterator<String> l = newLines.iterator(); l.hasNext();) {
284 				fw.write(l.next());
285 				if (l.hasNext()) {
286 					// Don't bother handling line endings - if it was Windows,
287 					// the \r is still there!
288 					fw.write('\n');
289 				}
290 			}
291 		}
292 		getRepository().getFS().setExecute(f, fh.getNewMode() == FileMode.EXECUTABLE_FILE);
293 	}
294 
295 	private boolean canApplyAt(List<String> hunkLines, List<String> newLines,
296 			int line) {
297 		int sz = hunkLines.size();
298 		int limit = newLines.size();
299 		int pos = line;
300 		for (int j = 1; j < sz; j++) {
301 			String hunkLine = hunkLines.get(j);
302 			switch (hunkLine.charAt(0)) {
303 			case ' ':
304 			case '-':
305 				if (pos >= limit
306 						|| !newLines.get(pos).equals(hunkLine.substring(1))) {
307 					return false;
308 				}
309 				pos++;
310 				break;
311 			default:
312 				break;
313 			}
314 		}
315 		return true;
316 	}
317 
318 	private static boolean isChanged(List<String> ol, List<String> nl) {
319 		if (ol.size() != nl.size())
320 			return true;
321 		for (int i = 0; i < ol.size(); i++)
322 			if (!ol.get(i).equals(nl.get(i)))
323 				return true;
324 		return false;
325 	}
326 
327 	private boolean isNoNewlineAtEndOfFile(FileHeader fh) {
328 		List<? extends HunkHeader> hunks = fh.getHunks();
329 		if (hunks == null || hunks.isEmpty()) {
330 			return false;
331 		}
332 		HunkHeader lastHunk = hunks.get(hunks.size() - 1);
333 		RawText lhrt = new RawText(lastHunk.getBuffer());
334 		return lhrt.getString(lhrt.size() - 1)
335 				.equals("\\ No newline at end of file"); //$NON-NLS-1$
336 	}
337 }