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.lib.Constants;
58  import org.eclipse.jgit.lib.ObjectId;
59  import org.eclipse.jgit.lib.ObjectReader;
60  import org.eclipse.jgit.lib.Ref;
61  import org.eclipse.jgit.lib.RefComparator;
62  import org.eclipse.jgit.lib.RefRename;
63  import org.eclipse.jgit.lib.RefUpdate;
64  import org.eclipse.jgit.lib.RefUpdate.Result;
65  import org.eclipse.jgit.lib.Repository;
66  import org.eclipse.jgit.pgm.internal.CLIText;
67  import org.eclipse.jgit.pgm.opt.OptionWithValuesListHandler;
68  import org.eclipse.jgit.revwalk.RevWalk;
69  import org.kohsuke.args4j.Argument;
70  import org.kohsuke.args4j.Option;
71  
72  @Command(common = true, usage = "usage_listCreateOrDeleteBranches")
73  class Branch extends TextBuiltin {
74  
75  	private String otherBranch;
76  	private boolean createForce;
77  	private boolean rename;
78  
79  	@Option(name = "--remote", aliases = { "-r" }, usage = "usage_actOnRemoteTrackingBranches")
80  	private boolean remote = false;
81  
82  	@Option(name = "--all", aliases = { "-a" }, usage = "usage_listBothRemoteTrackingAndLocalBranches")
83  	private boolean all = false;
84  
85  	@Option(name = "--contains", metaVar = "metaVar_commitish", usage = "usage_printOnlyBranchesThatContainTheCommit")
86  	private String containsCommitish;
87  
88  	private List<String> delete;
89  
90  	/**
91  	 * Delete branches
92  	 *
93  	 * @param names
94  	 *            a {@link java.util.List} of branch names.
95  	 */
96  	@Option(name = "--delete", aliases = {
97  			"-d" }, metaVar = "metaVar_branchNames", usage = "usage_deleteFullyMergedBranch", handler = OptionWithValuesListHandler.class)
98  	public void delete(List<String> names) {
99  		if (names.isEmpty()) {
100 			throw die(CLIText.get().branchNameRequired);
101 		}
102 		delete = names;
103 	}
104 
105 	private List<String> deleteForce;
106 
107 	/**
108 	 * Forcefully delete branches
109 	 *
110 	 * @param names
111 	 *            a {@link java.util.List} of branch names.
112 	 */
113 	@Option(name = "--delete-force", aliases = {
114 			"-D" }, metaVar = "metaVar_branchNames", usage = "usage_deleteBranchEvenIfNotMerged", handler = OptionWithValuesListHandler.class)
115 	public void deleteForce(List<String> names) {
116 		if (names.isEmpty()) {
117 			throw die(CLIText.get().branchNameRequired);
118 		}
119 		deleteForce = names;
120 	}
121 
122 	/**
123 	 * Forcefully create a list of branches
124 	 *
125 	 * @param branchAndStartPoint
126 	 *            a branch name and a start point
127 	 */
128 	@Option(name = "--create-force", aliases = {
129 			"-f" }, metaVar = "metaVar_branchAndStartPoint", usage = "usage_forceCreateBranchEvenExists", handler = OptionWithValuesListHandler.class)
130 	public void createForce(List<String> branchAndStartPoint) {
131 		createForce = true;
132 		if (branchAndStartPoint.isEmpty()) {
133 			throw die(CLIText.get().branchNameRequired);
134 		}
135 		if (branchAndStartPoint.size() > 2) {
136 			throw die(CLIText.get().tooManyRefsGiven);
137 		}
138 		if (branchAndStartPoint.size() == 1) {
139 			branch = branchAndStartPoint.get(0);
140 		} else {
141 			branch = branchAndStartPoint.get(0);
142 			otherBranch = branchAndStartPoint.get(1);
143 		}
144 	}
145 
146 	/**
147 	 * Move or rename a branch
148 	 *
149 	 * @param currentAndNew
150 	 *            the current and the new branch name
151 	 */
152 	@Option(name = "--move", aliases = {
153 			"-m" }, metaVar = "metaVar_oldNewBranchNames", usage = "usage_moveRenameABranch", handler = OptionWithValuesListHandler.class)
154 	public void moveRename(List<String> currentAndNew) {
155 		rename = true;
156 		if (currentAndNew.isEmpty()) {
157 			throw die(CLIText.get().branchNameRequired);
158 		}
159 		if (currentAndNew.size() > 2) {
160 			throw die(CLIText.get().tooManyRefsGiven);
161 		}
162 		if (currentAndNew.size() == 1) {
163 			branch = currentAndNew.get(0);
164 		} else {
165 			branch = currentAndNew.get(0);
166 			otherBranch = currentAndNew.get(1);
167 		}
168 	}
169 
170 	@Option(name = "--verbose", aliases = { "-v" }, usage = "usage_beVerbose")
171 	private boolean verbose = false;
172 
173 	@Argument(metaVar = "metaVar_name")
174 	private String branch;
175 
176 	private final Map<String, Ref> printRefs = new LinkedHashMap<>();
177 
178 	/** Only set for verbose branch listing at-the-moment */
179 	private RevWalk rw;
180 
181 	private int maxNameLength;
182 
183 	/** {@inheritDoc} */
184 	@Override
185 	protected void run() throws Exception {
186 		if (delete != null || deleteForce != null) {
187 			if (delete != null) {
188 				delete(delete, false);
189 			}
190 			if (deleteForce != null) {
191 				delete(deleteForce, true);
192 			}
193 		} else {
194 			if (rename) {
195 				String src, dst;
196 				if (otherBranch == null) {
197 					final Ref head = db.exactRef(Constants.HEAD);
198 					if (head != null && head.isSymbolic()) {
199 						src = head.getLeaf().getName();
200 					} else {
201 						throw die(CLIText.get().cannotRenameDetachedHEAD);
202 					}
203 					dst = branch;
204 				} else {
205 					src = branch;
206 					final Ref old = db.findRef(src);
207 					if (old == null)
208 						throw die(MessageFormat.format(CLIText.get().doesNotExist, src));
209 					if (!old.getName().startsWith(Constants.R_HEADS))
210 						throw die(MessageFormat.format(CLIText.get().notABranch, src));
211 					src = old.getName();
212 					dst = otherBranch;
213 				}
214 
215 				if (!dst.startsWith(Constants.R_HEADS))
216 					dst = Constants.R_HEADS + dst;
217 				if (!Repository.isValidRefName(dst))
218 					throw die(MessageFormat.format(CLIText.get().notAValidRefName, dst));
219 
220 				RefRename r = db.renameRef(src, dst);
221 				if (r.rename() != Result.RENAMED)
222 					throw die(MessageFormat.format(CLIText.get().cannotBeRenamed, src));
223 
224 			} else if (createForce || branch != null) {
225 				String newHead = branch;
226 				String startBranch;
227 				if (createForce) {
228 					startBranch = otherBranch;
229 				} else {
230 					startBranch = Constants.HEAD;
231 				}
232 				Ref startRef = db.findRef(startBranch);
233 				ObjectId startAt = db.resolve(startBranch + "^0"); //$NON-NLS-1$
234 				if (startRef != null) {
235 					startBranch = startRef.getName();
236 				} else if (startAt != null) {
237 					startBranch = startAt.name();
238 				} else {
239 					throw die(MessageFormat.format(
240 							CLIText.get().notAValidCommitName, startBranch));
241 				}
242 				startBranch = Repository.shortenRefName(startBranch);
243 				String newRefName = newHead;
244 				if (!newRefName.startsWith(Constants.R_HEADS)) {
245 					newRefName = Constants.R_HEADS + newRefName;
246 				}
247 				if (!Repository.isValidRefName(newRefName)) {
248 					throw die(MessageFormat.format(CLIText.get().notAValidRefName, newRefName));
249 				}
250 				if (!createForce && db.resolve(newRefName) != null) {
251 					throw die(MessageFormat.format(CLIText.get().branchAlreadyExists, newHead));
252 				}
253 				RefUpdate updateRef = db.updateRef(newRefName);
254 				updateRef.setNewObjectId(startAt);
255 				updateRef.setForceUpdate(createForce);
256 				updateRef.setRefLogMessage(MessageFormat.format(CLIText.get().branchCreatedFrom, startBranch), false);
257 				Result update = updateRef.update();
258 				if (update == Result.REJECTED) {
259 					throw die(MessageFormat.format(CLIText.get().couldNotCreateBranch, newHead, update.toString()));
260 				}
261 			} else {
262 				if (verbose) {
263 					rw = new RevWalk(db);
264 				}
265 				list();
266 			}
267 		}
268 	}
269 
270 	private void list() throws Exception {
271 		Ref head = db.exactRef(Constants.HEAD);
272 		// This can happen if HEAD is stillborn
273 		if (head != null) {
274 			String current = head.getLeaf().getName();
275 			try (Git git = new Git(db)) {
276 				ListBranchCommand command = git.branchList();
277 				if (all)
278 					command.setListMode(ListMode.ALL);
279 				else if (remote)
280 					command.setListMode(ListMode.REMOTE);
281 
282 				if (containsCommitish != null)
283 					command.setContains(containsCommitish);
284 
285 				List<Ref> refs = command.call();
286 				for (Ref ref : refs) {
287 					if (ref.getName().equals(Constants.HEAD))
288 						addRef("(no branch)", head); //$NON-NLS-1$
289 				}
290 
291 				addRefs(refs, Constants.R_HEADS);
292 				addRefs(refs, Constants.R_REMOTES);
293 
294 				try (ObjectReader reader = db.newObjectReader()) {
295 					for (Entry<String, Ref> e : printRefs.entrySet()) {
296 						final Ref ref = e.getValue();
297 						printHead(reader, e.getKey(),
298 								current.equals(ref.getName()), ref);
299 					}
300 				}
301 			}
302 		}
303 	}
304 
305 	private void addRefs(Collection<Ref> refs, String prefix) {
306 		for (Ref ref : RefComparator.sort(refs)) {
307 			final String name = ref.getName();
308 			if (name.startsWith(prefix))
309 				addRef(name.substring(name.indexOf('/', 5) + 1), ref);
310 		}
311 	}
312 
313 	private void addRef(String name, Ref ref) {
314 		printRefs.put(name, ref);
315 		maxNameLength = Math.max(maxNameLength, name.length());
316 	}
317 
318 	private void printHead(final ObjectReader reader, final String ref,
319 			final boolean isCurrent, final Ref refObj) throws Exception {
320 		outw.print(isCurrent ? '*' : ' ');
321 		outw.print(' ');
322 		outw.print(ref);
323 		if (verbose) {
324 			final int spaces = maxNameLength - ref.length() + 1;
325 			outw.format("%" + spaces + "s", ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
326 			final ObjectId objectId = refObj.getObjectId();
327 			outw.print(reader.abbreviate(objectId).name());
328 			outw.print(' ');
329 			outw.print(rw.parseCommit(objectId).getShortMessage());
330 		}
331 		outw.println();
332 	}
333 
334 	private void delete(List<String> branches, boolean force)
335 			throws IOException {
336 		String current = db.getBranch();
337 		ObjectId head = db.resolve(Constants.HEAD);
338 		for (String b : branches) {
339 			if (b.equals(current)) {
340 				throw die(MessageFormat.format(CLIText.get().cannotDeleteTheBranchWhichYouAreCurrentlyOn, b));
341 			}
342 			RefUpdate update = db.updateRef((remote ? Constants.R_REMOTES
343 					: Constants.R_HEADS)
344 					+ b);
345 			update.setNewObjectId(head);
346 			update.setForceUpdate(force || remote);
347 			Result result = update.delete();
348 			if (result == Result.REJECTED) {
349 				throw die(MessageFormat.format(CLIText.get().branchIsNotAnAncestorOfYourCurrentHEAD, b));
350 			} else if (result == Result.NEW)
351 				throw die(MessageFormat.format(CLIText.get().branchNotFound, b));
352 			if (remote)
353 				outw.println(MessageFormat.format(CLIText.get().deletedRemoteBranch, b));
354 			else if (verbose)
355 				outw.println(MessageFormat.format(CLIText.get().deletedBranch, b));
356 		}
357 	}
358 }