View Javadoc
1   /*
2    * Copyright (C) 2007-2008, Charles O'Farrell <charleso@charleso.org>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.pgm;
45  
46  import java.io.IOException;
47  import java.text.MessageFormat;
48  import java.util.Collection;
49  import java.util.LinkedHashMap;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Map.Entry;
53  
54  import org.eclipse.jgit.api.Git;
55  import org.eclipse.jgit.api.ListBranchCommand;
56  import org.eclipse.jgit.api.ListBranchCommand.ListMode;
57  import org.eclipse.jgit.api.errors.GitAPIException;
58  import org.eclipse.jgit.lib.Constants;
59  import org.eclipse.jgit.lib.ObjectId;
60  import org.eclipse.jgit.lib.ObjectReader;
61  import org.eclipse.jgit.lib.Ref;
62  import org.eclipse.jgit.lib.RefComparator;
63  import org.eclipse.jgit.lib.RefRename;
64  import org.eclipse.jgit.lib.RefUpdate;
65  import org.eclipse.jgit.lib.RefUpdate.Result;
66  import org.eclipse.jgit.lib.Repository;
67  import org.eclipse.jgit.pgm.internal.CLIText;
68  import org.eclipse.jgit.pgm.opt.OptionWithValuesListHandler;
69  import org.eclipse.jgit.revwalk.RevWalk;
70  import org.kohsuke.args4j.Argument;
71  import org.kohsuke.args4j.Option;
72  
73  @Command(common = true, usage = "usage_listCreateOrDeleteBranches")
74  class Branch extends TextBuiltin {
75  
76  	private String otherBranch;
77  	private boolean createForce;
78  	private boolean rename;
79  
80  	@Option(name = "--remote", aliases = { "-r" }, usage = "usage_actOnRemoteTrackingBranches")
81  	private boolean remote = false;
82  
83  	@Option(name = "--all", aliases = { "-a" }, usage = "usage_listBothRemoteTrackingAndLocalBranches")
84  	private boolean all = false;
85  
86  	@Option(name = "--contains", metaVar = "metaVar_commitish", usage = "usage_printOnlyBranchesThatContainTheCommit")
87  	private String containsCommitish;
88  
89  	private List<String> delete;
90  
91  	/**
92  	 * Delete branches
93  	 *
94  	 * @param names
95  	 *            a {@link java.util.List} of branch names.
96  	 */
97  	@Option(name = "--delete", aliases = {
98  			"-d" }, metaVar = "metaVar_branchNames", usage = "usage_deleteFullyMergedBranch", handler = OptionWithValuesListHandler.class)
99  	public void delete(List<String> names) {
100 		if (names.isEmpty()) {
101 			throw die(CLIText.get().branchNameRequired);
102 		}
103 		delete = names;
104 	}
105 
106 	private List<String> deleteForce;
107 
108 	/**
109 	 * Forcefully delete branches
110 	 *
111 	 * @param names
112 	 *            a {@link java.util.List} of branch names.
113 	 */
114 	@Option(name = "--delete-force", aliases = {
115 			"-D" }, metaVar = "metaVar_branchNames", usage = "usage_deleteBranchEvenIfNotMerged", handler = OptionWithValuesListHandler.class)
116 	public void deleteForce(List<String> names) {
117 		if (names.isEmpty()) {
118 			throw die(CLIText.get().branchNameRequired);
119 		}
120 		deleteForce = names;
121 	}
122 
123 	/**
124 	 * Forcefully create a list of branches
125 	 *
126 	 * @param branchAndStartPoint
127 	 *            a branch name and a start point
128 	 */
129 	@Option(name = "--create-force", aliases = {
130 			"-f" }, metaVar = "metaVar_branchAndStartPoint", usage = "usage_forceCreateBranchEvenExists", handler = OptionWithValuesListHandler.class)
131 	public void createForce(List<String> branchAndStartPoint) {
132 		createForce = true;
133 		if (branchAndStartPoint.isEmpty()) {
134 			throw die(CLIText.get().branchNameRequired);
135 		}
136 		if (branchAndStartPoint.size() > 2) {
137 			throw die(CLIText.get().tooManyRefsGiven);
138 		}
139 		if (branchAndStartPoint.size() == 1) {
140 			branch = branchAndStartPoint.get(0);
141 		} else {
142 			branch = branchAndStartPoint.get(0);
143 			otherBranch = branchAndStartPoint.get(1);
144 		}
145 	}
146 
147 	/**
148 	 * Move or rename a branch
149 	 *
150 	 * @param currentAndNew
151 	 *            the current and the new branch name
152 	 */
153 	@Option(name = "--move", aliases = {
154 			"-m" }, metaVar = "metaVar_oldNewBranchNames", usage = "usage_moveRenameABranch", handler = OptionWithValuesListHandler.class)
155 	public void moveRename(List<String> currentAndNew) {
156 		rename = true;
157 		if (currentAndNew.isEmpty()) {
158 			throw die(CLIText.get().branchNameRequired);
159 		}
160 		if (currentAndNew.size() > 2) {
161 			throw die(CLIText.get().tooManyRefsGiven);
162 		}
163 		if (currentAndNew.size() == 1) {
164 			branch = currentAndNew.get(0);
165 		} else {
166 			branch = currentAndNew.get(0);
167 			otherBranch = currentAndNew.get(1);
168 		}
169 	}
170 
171 	@Option(name = "--verbose", aliases = { "-v" }, usage = "usage_beVerbose")
172 	private boolean verbose = false;
173 
174 	@Argument(metaVar = "metaVar_name")
175 	private String branch;
176 
177 	private final Map<String, Ref> printRefs = new LinkedHashMap<>();
178 
179 	/** Only set for verbose branch listing at-the-moment */
180 	private RevWalk rw;
181 
182 	private int maxNameLength;
183 
184 	/** {@inheritDoc} */
185 	@Override
186 	protected void run() {
187 		try {
188 			if (delete != null || deleteForce != null) {
189 				if (delete != null) {
190 					delete(delete, false);
191 				}
192 				if (deleteForce != null) {
193 					delete(deleteForce, true);
194 				}
195 				return;
196 			}
197 			if (rename) {
198 				String src, dst;
199 				if (otherBranch == null) {
200 					final Ref head = db.exactRef(Constants.HEAD);
201 					if (head != null && head.isSymbolic()) {
202 						src = head.getLeaf().getName();
203 					} else {
204 						throw die(CLIText.get().cannotRenameDetachedHEAD);
205 					}
206 					dst = branch;
207 				} else {
208 					src = branch;
209 					final Ref old = db.findRef(src);
210 					if (old == null) {
211 						throw die(MessageFormat.format(CLIText.get().doesNotExist, src));
212 					}
213 					if (!old.getName().startsWith(Constants.R_HEADS)) {
214 						throw die(MessageFormat.format(CLIText.get().notABranch, src));
215 					}
216 					src = old.getName();
217 					dst = otherBranch;
218 				}
219 
220 				if (!dst.startsWith(Constants.R_HEADS)) {
221 					dst = Constants.R_HEADS + dst;
222 				}
223 				if (!Repository.isValidRefName(dst)) {
224 					throw die(MessageFormat.format(CLIText.get().notAValidRefName, dst));
225 				}
226 
227 				RefRename r = db.renameRef(src, dst);
228 				if (r.rename() != Result.RENAMED) {
229 					throw die(MessageFormat.format(CLIText.get().cannotBeRenamed, src));
230 				}
231 
232 			} else if (createForce || branch != null) {
233 				String newHead = branch;
234 				String startBranch;
235 				if (createForce) {
236 					startBranch = otherBranch;
237 				} else {
238 					startBranch = Constants.HEAD;
239 				}
240 				Ref startRef = db.findRef(startBranch);
241 				ObjectId startAt = db.resolve(startBranch + "^0"); //$NON-NLS-1$
242 				if (startRef != null) {
243 					startBranch = startRef.getName();
244 				} else if (startAt != null) {
245 					startBranch = startAt.name();
246 				} else {
247 					throw die(MessageFormat.format(
248 							CLIText.get().notAValidCommitName, startBranch));
249 				}
250 				startBranch = Repository.shortenRefName(startBranch);
251 				String newRefName = newHead;
252 				if (!newRefName.startsWith(Constants.R_HEADS)) {
253 					newRefName = Constants.R_HEADS + newRefName;
254 				}
255 				if (!Repository.isValidRefName(newRefName)) {
256 					throw die(MessageFormat.format(CLIText.get().notAValidRefName, newRefName));
257 				}
258 				if (!createForce && db.resolve(newRefName) != null) {
259 					throw die(MessageFormat.format(CLIText.get().branchAlreadyExists, newHead));
260 				}
261 				RefUpdate updateRef = db.updateRef(newRefName);
262 				updateRef.setNewObjectId(startAt);
263 				updateRef.setForceUpdate(createForce);
264 				updateRef.setRefLogMessage(MessageFormat.format(CLIText.get().branchCreatedFrom, startBranch), false);
265 				Result update = updateRef.update();
266 				if (update == Result.REJECTED) {
267 					throw die(MessageFormat.format(CLIText.get().couldNotCreateBranch, newHead, update.toString()));
268 				}
269 			} else {
270 				if (verbose) {
271 					rw = new RevWalk(db);
272 				}
273 				list();
274 			}
275 		} catch (IOException | GitAPIException e) {
276 			throw die(e.getMessage(), e);
277 		}
278 	}
279 
280 	private void list() throws IOException, GitAPIException {
281 		Ref head = db.exactRef(Constants.HEAD);
282 		// This can happen if HEAD is stillborn
283 		if (head != null) {
284 			String current = head.getLeaf().getName();
285 			try (Gitit.html#Git">Git git = new Git(db)) {
286 				ListBranchCommand command = git.branchList();
287 				if (all)
288 					command.setListMode(ListMode.ALL);
289 				else if (remote)
290 					command.setListMode(ListMode.REMOTE);
291 
292 				if (containsCommitish != null)
293 					command.setContains(containsCommitish);
294 
295 				List<Ref> refs = command.call();
296 				for (Ref ref : refs) {
297 					if (ref.getName().equals(Constants.HEAD))
298 						addRef("(no branch)", head); //$NON-NLS-1$
299 				}
300 
301 				addRefs(refs, Constants.R_HEADS);
302 				addRefs(refs, Constants.R_REMOTES);
303 
304 				try (ObjectReader reader = db.newObjectReader()) {
305 					for (Entry<String, Ref> e : printRefs.entrySet()) {
306 						final Ref ref = e.getValue();
307 						printHead(reader, e.getKey(),
308 								current.equals(ref.getName()), ref);
309 					}
310 				}
311 			}
312 		}
313 	}
314 
315 	private void addRefs(Collection<Ref> refs, String prefix) {
316 		for (Ref ref : RefComparator.sort(refs)) {
317 			final String name = ref.getName();
318 			if (name.startsWith(prefix))
319 				addRef(name.substring(name.indexOf('/', 5) + 1), ref);
320 		}
321 	}
322 
323 	private void addRef(String name, Ref ref) {
324 		printRefs.put(name, ref);
325 		maxNameLength = Math.max(maxNameLength, name.length());
326 	}
327 
328 	private void printHead(final ObjectReader reader, final String ref,
329 			final boolean isCurrent, final Ref refObj) throws IOException {
330 		outw.print(isCurrent ? '*' : ' ');
331 		outw.print(' ');
332 		outw.print(ref);
333 		if (verbose) {
334 			final int spaces = maxNameLength - ref.length() + 1;
335 			outw.format("%" + spaces + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
336 			final ObjectId objectId = refObj.getObjectId();
337 			outw.print(reader.abbreviate(objectId).name());
338 			outw.print(' ');
339 			outw.print(rw.parseCommit(objectId).getShortMessage());
340 		}
341 		outw.println();
342 	}
343 
344 	private void delete(List<String> branches, boolean force)
345 			throws IOException {
346 		String current = db.getBranch();
347 		ObjectId head = db.resolve(Constants.HEAD);
348 		for (String b : branches) {
349 			if (b.equals(current)) {
350 				throw die(MessageFormat.format(CLIText.get().cannotDeleteTheBranchWhichYouAreCurrentlyOn, b));
351 			}
352 			RefUpdate update = db.updateRef((remote ? Constants.R_REMOTES
353 					: Constants.R_HEADS)
354 					+ b);
355 			update.setNewObjectId(head);
356 			update.setForceUpdate(force || remote);
357 			Result result = update.delete();
358 			if (result == Result.REJECTED) {
359 				throw die(MessageFormat.format(CLIText.get().branchIsNotAnAncestorOfYourCurrentHEAD, b));
360 			} else if (result == Result.NEW)
361 				throw die(MessageFormat.format(CLIText.get().branchNotFound, b));
362 			if (remote)
363 				outw.println(MessageFormat.format(CLIText.get().deletedRemoteBranch, b));
364 			else if (verbose)
365 				outw.println(MessageFormat.format(CLIText.get().deletedBranch, b));
366 		}
367 	}
368 }