View Javadoc
1   /*
2    * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
3    * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.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.HEAD;
14  import static org.eclipse.jgit.lib.Constants.R_HEADS;
15  
16  import java.io.IOException;
17  import java.text.MessageFormat;
18  
19  import org.eclipse.jgit.api.errors.GitAPIException;
20  import org.eclipse.jgit.api.errors.InvalidRefNameException;
21  import org.eclipse.jgit.api.errors.JGitInternalException;
22  import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
23  import org.eclipse.jgit.api.errors.RefNotFoundException;
24  import org.eclipse.jgit.errors.AmbiguousObjectException;
25  import org.eclipse.jgit.internal.JGitText;
26  import org.eclipse.jgit.lib.ConfigConstants;
27  import org.eclipse.jgit.lib.Constants;
28  import org.eclipse.jgit.lib.ObjectId;
29  import org.eclipse.jgit.lib.Ref;
30  import org.eclipse.jgit.lib.RefUpdate;
31  import org.eclipse.jgit.lib.RefUpdate.Result;
32  import org.eclipse.jgit.lib.Repository;
33  import org.eclipse.jgit.lib.StoredConfig;
34  import org.eclipse.jgit.revwalk.RevCommit;
35  import org.eclipse.jgit.revwalk.RevWalk;
36  
37  /**
38   * Used to create a local branch.
39   *
40   * @see <a
41   *      href="http://www.kernel.org/pub/software/scm/git/docs/git-branch.html"
42   *      >Git documentation about Branch</a>
43   */
44  public class CreateBranchCommand extends GitCommand<Ref> {
45  	private String name;
46  
47  	private boolean force = false;
48  
49  	private SetupUpstreamMode upstreamMode;
50  
51  	private String startPoint = HEAD;
52  
53  	private RevCommit startCommit;
54  
55  	/**
56  	 * The modes available for setting up the upstream configuration
57  	 * (corresponding to the --set-upstream, --track, --no-track options
58  	 *
59  	 */
60  	public enum SetupUpstreamMode {
61  		/**
62  		 * Corresponds to the --track option
63  		 */
64  		TRACK,
65  		/**
66  		 * Corresponds to the --no-track option
67  		 */
68  		NOTRACK,
69  		/**
70  		 * Corresponds to the --set-upstream option
71  		 */
72  		SET_UPSTREAM;
73  	}
74  
75  	/**
76  	 * Constructor for CreateBranchCommand
77  	 *
78  	 * @param repo
79  	 *            the {@link org.eclipse.jgit.lib.Repository}
80  	 */
81  	protected CreateBranchCommand(Repository repo) {
82  		super(repo);
83  	}
84  
85  	/** {@inheritDoc} */
86  	@Override
87  	public Ref call() throws GitAPIException, RefAlreadyExistsException,
88  			RefNotFoundException, InvalidRefNameException {
89  		checkCallable();
90  		processOptions();
91  		try (RevWalk revWalk = new RevWalk(repo)) {
92  			Ref refToCheck = repo.findRef(name);
93  			boolean exists = refToCheck != null
94  					&& refToCheck.getName().startsWith(R_HEADS);
95  			if (!force && exists)
96  				throw new RefAlreadyExistsException(MessageFormat.format(
97  						JGitText.get().refAlreadyExists1, name));
98  
99  			ObjectId startAt = getStartPointObjectId();
100 			String startPointFullName = null;
101 			if (startPoint != null) {
102 				Ref baseRef = repo.findRef(startPoint);
103 				if (baseRef != null)
104 					startPointFullName = baseRef.getName();
105 			}
106 
107 			// determine whether we are based on a commit,
108 			// a branch, or a tag and compose the reflog message
109 			String refLogMessage;
110 			String baseBranch = ""; //$NON-NLS-1$
111 			if (startPointFullName == null) {
112 				String baseCommit;
113 				if (startCommit != null)
114 					baseCommit = startCommit.getShortMessage();
115 				else {
116 					RevCommit commit = revWalk.parseCommit(repo
117 							.resolve(getStartPointOrHead()));
118 					baseCommit = commit.getShortMessage();
119 				}
120 				if (exists)
121 					refLogMessage = "branch: Reset start-point to commit " //$NON-NLS-1$
122 							+ baseCommit;
123 				else
124 					refLogMessage = "branch: Created from commit " + baseCommit; //$NON-NLS-1$
125 
126 			} else if (startPointFullName.startsWith(R_HEADS)
127 					|| startPointFullName.startsWith(Constants.R_REMOTES)) {
128 				baseBranch = startPointFullName;
129 				if (exists)
130 					refLogMessage = "branch: Reset start-point to branch " //$NON-NLS-1$
131 							+ startPointFullName; // TODO
132 				else
133 					refLogMessage = "branch: Created from branch " + baseBranch; //$NON-NLS-1$
134 			} else {
135 				startAt = revWalk.peel(revWalk.parseAny(startAt));
136 				if (exists)
137 					refLogMessage = "branch: Reset start-point to tag " //$NON-NLS-1$
138 							+ startPointFullName;
139 				else
140 					refLogMessage = "branch: Created from tag " //$NON-NLS-1$
141 							+ startPointFullName;
142 			}
143 
144 			RefUpdate updateRef = repo.updateRef(R_HEADS + name);
145 			updateRef.setNewObjectId(startAt);
146 			updateRef.setRefLogMessage(refLogMessage, false);
147 			Result updateResult;
148 			if (exists && force)
149 				updateResult = updateRef.forceUpdate();
150 			else
151 				updateResult = updateRef.update();
152 
153 			setCallable(false);
154 
155 			boolean ok = false;
156 			switch (updateResult) {
157 			case NEW:
158 				ok = !exists;
159 				break;
160 			case NO_CHANGE:
161 			case FAST_FORWARD:
162 			case FORCED:
163 				ok = exists;
164 				break;
165 			default:
166 				break;
167 			}
168 
169 			if (!ok)
170 				throw new JGitInternalException(MessageFormat.format(JGitText
171 						.get().createBranchUnexpectedResult, updateResult
172 						.name()));
173 
174 			Ref result = repo.findRef(name);
175 			if (result == null)
176 				throw new JGitInternalException(
177 						JGitText.get().createBranchFailedUnknownReason);
178 
179 			if (baseBranch.length() == 0) {
180 				return result;
181 			}
182 
183 			// if we are based on another branch, see
184 			// if we need to configure upstream configuration: first check
185 			// whether the setting was done explicitly
186 			boolean doConfigure;
187 			if (upstreamMode == SetupUpstreamMode.SET_UPSTREAM
188 					|| upstreamMode == SetupUpstreamMode.TRACK)
189 				// explicitly set to configure
190 				doConfigure = true;
191 			else if (upstreamMode == SetupUpstreamMode.NOTRACK)
192 				// explicitly set to not configure
193 				doConfigure = false;
194 			else {
195 				// if there was no explicit setting, check the configuration
196 				String autosetupflag = repo.getConfig().getString(
197 						ConfigConstants.CONFIG_BRANCH_SECTION, null,
198 						ConfigConstants.CONFIG_KEY_AUTOSETUPMERGE);
199 				if ("false".equals(autosetupflag)) { //$NON-NLS-1$
200 					doConfigure = false;
201 				} else if ("always".equals(autosetupflag)) { //$NON-NLS-1$
202 					doConfigure = true;
203 				} else {
204 					// in this case, the default is to configure
205 					// only in case the base branch was a remote branch
206 					doConfigure = baseBranch.startsWith(Constants.R_REMOTES);
207 				}
208 			}
209 
210 			if (doConfigure) {
211 				StoredConfig config = repo.getConfig();
212 
213 				String remoteName = repo.getRemoteName(baseBranch);
214 				if (remoteName != null) {
215 					String branchName = repo
216 							.shortenRemoteBranchName(baseBranch);
217 					config
218 							.setString(ConfigConstants.CONFIG_BRANCH_SECTION,
219 									name, ConfigConstants.CONFIG_KEY_REMOTE,
220 									remoteName);
221 					config.setString(ConfigConstants.CONFIG_BRANCH_SECTION,
222 							name, ConfigConstants.CONFIG_KEY_MERGE,
223 							Constants.R_HEADS + branchName);
224 				} else {
225 					// set "." as remote
226 					config.setString(ConfigConstants.CONFIG_BRANCH_SECTION,
227 							name, ConfigConstants.CONFIG_KEY_REMOTE, "."); //$NON-NLS-1$
228 					config.setString(ConfigConstants.CONFIG_BRANCH_SECTION,
229 							name, ConfigConstants.CONFIG_KEY_MERGE, baseBranch);
230 				}
231 				config.save();
232 			}
233 			return result;
234 		} catch (IOException ioe) {
235 			throw new JGitInternalException(ioe.getMessage(), ioe);
236 		}
237 	}
238 
239 	private ObjectId getStartPointObjectId() throws AmbiguousObjectException,
240 			RefNotFoundException, IOException {
241 		if (startCommit != null)
242 			return startCommit.getId();
243 		String startPointOrHead = getStartPointOrHead();
244 		ObjectId result = repo.resolve(startPointOrHead);
245 		if (result == null)
246 			throw new RefNotFoundException(MessageFormat.format(
247 					JGitText.get().refNotResolved, startPointOrHead));
248 		return result;
249 	}
250 
251 	private String getStartPointOrHead() {
252 		return startPoint != null ? startPoint : HEAD;
253 	}
254 
255 	private void processOptions() throws InvalidRefNameException {
256 		if (name == null
257 				|| !Repository.isValidRefName(R_HEADS + name)
258 				|| !isValidBranchName(name))
259 			throw new InvalidRefNameException(MessageFormat.format(JGitText
260 					.get().branchNameInvalid, name == null ? "<null>" : name)); //$NON-NLS-1$
261 	}
262 
263 	/**
264 	 * Check if the given branch name is valid
265 	 *
266 	 * @param branchName
267 	 *            branch name to check
268 	 * @return {@code true} if the branch name is valid
269 	 *
270 	 * @since 5.0
271 	 */
272 	public static boolean isValidBranchName(String branchName) {
273 		if (HEAD.equals(branchName)) {
274 			return false;
275 		}
276 		return !branchName.startsWith("-"); //$NON-NLS-1$
277 	}
278 
279 	/**
280 	 * Set the name of the new branch
281 	 *
282 	 * @param name
283 	 *            the name of the new branch
284 	 * @return this instance
285 	 */
286 	public CreateBranchCommand setName(String name) {
287 		checkCallable();
288 		this.name = name;
289 		return this;
290 	}
291 
292 	/**
293 	 * Set whether to create the branch forcefully
294 	 *
295 	 * @param force
296 	 *            if <code>true</code> and the branch with the given name
297 	 *            already exists, the start-point of an existing branch will be
298 	 *            set to a new start-point; if false, the existing branch will
299 	 *            not be changed
300 	 * @return this instance
301 	 */
302 	public CreateBranchCommand setForce(boolean force) {
303 		checkCallable();
304 		this.force = force;
305 		return this;
306 	}
307 
308 	/**
309 	 * Set the start point
310 	 *
311 	 * @param startPoint
312 	 *            corresponds to the start-point option; if <code>null</code>,
313 	 *            the current HEAD will be used
314 	 * @return this instance
315 	 */
316 	public CreateBranchCommand setStartPoint(String startPoint) {
317 		checkCallable();
318 		this.startPoint = startPoint;
319 		this.startCommit = null;
320 		return this;
321 	}
322 
323 	/**
324 	 * Set the start point
325 	 *
326 	 * @param startPoint
327 	 *            corresponds to the start-point option; if <code>null</code>,
328 	 *            the current HEAD will be used
329 	 * @return this instance
330 	 */
331 	public CreateBranchCommand setStartPoint(RevCommit startPoint) {
332 		checkCallable();
333 		this.startCommit = startPoint;
334 		this.startPoint = null;
335 		return this;
336 	}
337 
338 	/**
339 	 * Set the upstream mode
340 	 *
341 	 * @param mode
342 	 *            corresponds to the --track/--no-track/--set-upstream options;
343 	 *            may be <code>null</code>
344 	 * @return this instance
345 	 */
346 	public CreateBranchCommand setUpstreamMode(SetupUpstreamMode mode) {
347 		checkCallable();
348 		this.upstreamMode = mode;
349 		return this;
350 	}
351 }