View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc.
3    * Copyright (C) 2009, Robin Rosenberg 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  
12  package org.eclipse.jgit.internal.storage.file;
13  
14  import java.io.File;
15  import java.io.IOException;
16  import java.nio.file.AtomicMoveNotSupportedException;
17  import java.nio.file.StandardCopyOption;
18  
19  import org.eclipse.jgit.lib.Constants;
20  import org.eclipse.jgit.lib.ObjectId;
21  import org.eclipse.jgit.lib.RefRename;
22  import org.eclipse.jgit.lib.RefUpdate;
23  import org.eclipse.jgit.lib.RefUpdate.Result;
24  import org.eclipse.jgit.revwalk.RevWalk;
25  import org.eclipse.jgit.util.FileUtils;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  /**
30   * Rename any reference stored by {@link RefDirectory}.
31   * <p>
32   * This class works by first renaming the source reference to a temporary name,
33   * then renaming the temporary name to the final destination reference.
34   * <p>
35   * This strategy permits switching a reference like {@code refs/heads/foo},
36   * which is a file, to {@code refs/heads/foo/bar}, which is stored inside a
37   * directory that happens to match the source name.
38   */
39  class RefDirectoryRename extends RefRename {
40  	private static final Logger LOG = LoggerFactory
41  			.getLogger(RefDirectoryRename.class);
42  
43  	private final RefDirectory refdb;
44  
45  	/**
46  	 * The value of the source reference at the start of the rename.
47  	 * <p>
48  	 * At the end of the rename the destination reference must have this same
49  	 * value, otherwise we have a concurrent update and the rename must fail
50  	 * without making any changes.
51  	 */
52  	private ObjectId objId;
53  
54  	/** True if HEAD must be moved to the destination reference. */
55  	private boolean updateHEAD;
56  
57  	/** A reference we backup {@link #objId} into during the rename. */
58  	private RefDirectoryUpdate tmp;
59  
60  	RefDirectoryRename(RefDirectoryUpdate./../../../org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.html#RefDirectoryUpdate">RefDirectoryUpdate src, RefDirectoryUpdate dst) {
61  		super(src, dst);
62  		refdb = src.getRefDatabase();
63  	}
64  
65  	/** {@inheritDoc} */
66  	@Override
67  	protected Result doRename() throws IOException {
68  		if (source.getRef().isSymbolic())
69  			return Result.IO_FAILURE; // not supported
70  
71  		objId = source.getOldObjectId();
72  		updateHEAD = needToUpdateHEAD();
73  		tmp = refdb.newTemporaryUpdate();
74  		try (RevWalkvwalk/RevWalk.html#RevWalk">RevWalk rw = new RevWalk(refdb.getRepository())) {
75  			// First backup the source so its never unreachable.
76  			tmp.setNewObjectId(objId);
77  			tmp.setForceUpdate(true);
78  			tmp.disableRefLog();
79  			switch (tmp.update(rw)) {
80  			case NEW:
81  			case FORCED:
82  			case NO_CHANGE:
83  				break;
84  			default:
85  				return tmp.getResult();
86  			}
87  
88  			// Save the source's log under the temporary name, we must do
89  			// this before we delete the source, otherwise we lose the log.
90  			if (!renameLog(source, tmp))
91  				return Result.IO_FAILURE;
92  
93  			// If HEAD has to be updated, link it now to destination.
94  			// We have to link before we delete, otherwise the delete
95  			// fails because its the current branch.
96  			RefUpdate dst = destination;
97  			if (updateHEAD) {
98  				if (!linkHEAD(destination)) {
99  					renameLog(tmp, source);
100 					return Result.LOCK_FAILURE;
101 				}
102 
103 				// Replace the update operation so HEAD will log the rename.
104 				dst = refdb.newUpdate(Constants.HEAD, false);
105 				dst.setRefLogIdent(destination.getRefLogIdent());
106 				dst.setRefLogMessage(destination.getRefLogMessage(), false);
107 			}
108 
109 			// Delete the source name so its path is free for replacement.
110 			source.setExpectedOldObjectId(objId);
111 			source.setForceUpdate(true);
112 			source.disableRefLog();
113 			if (source.delete(rw) != Result.FORCED) {
114 				renameLog(tmp, source);
115 				if (updateHEAD)
116 					linkHEAD(source);
117 				return source.getResult();
118 			}
119 
120 			// Move the log to the destination.
121 			if (!renameLog(tmp, destination)) {
122 				renameLog(tmp, source);
123 				source.setExpectedOldObjectId(ObjectId.zeroId());
124 				source.setNewObjectId(objId);
125 				source.update(rw);
126 				if (updateHEAD)
127 					linkHEAD(source);
128 				return Result.IO_FAILURE;
129 			}
130 
131 			// Create the destination, logging the rename during the creation.
132 			dst.setExpectedOldObjectId(ObjectId.zeroId());
133 			dst.setNewObjectId(objId);
134 			if (dst.update(rw) != Result.NEW) {
135 				// If we didn't create the destination we have to undo
136 				// our work. Put the log back and restore source.
137 				if (renameLog(destination, tmp))
138 					renameLog(tmp, source);
139 				source.setExpectedOldObjectId(ObjectId.zeroId());
140 				source.setNewObjectId(objId);
141 				source.update(rw);
142 				if (updateHEAD)
143 					linkHEAD(source);
144 				return dst.getResult();
145 			}
146 
147 			return Result.RENAMED;
148 		} finally {
149 			// Always try to free the temporary name.
150 			try {
151 				refdb.delete(tmp);
152 			} catch (IOException err) {
153 				FileUtils.delete(refdb.fileFor(tmp.getName()));
154 			}
155 		}
156 	}
157 
158 	private boolean renameLog(RefUpdate="../../../../../../org/eclipse/jgit/lib/RefUpdate.html#RefUpdate">RefUpdate src, RefUpdate dst) {
159 		File srcLog = refdb.logFor(src.getName());
160 		File dstLog = refdb.logFor(dst.getName());
161 
162 		if (!srcLog.exists())
163 			return true;
164 
165 		if (!rename(srcLog, dstLog))
166 			return false;
167 
168 		try {
169 			final int levels = RefDirectory.levelsIn(src.getName()) - 2;
170 			RefDirectory.delete(srcLog, levels);
171 			return true;
172 		} catch (IOException e) {
173 			rename(dstLog, srcLog);
174 			return false;
175 		}
176 	}
177 
178 	private static boolean rename(File src, File dst) {
179 		try {
180 			FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
181 			return true;
182 		} catch (AtomicMoveNotSupportedException e) {
183 			LOG.error(e.getMessage(), e);
184 		} catch (IOException e) {
185 			// ignore
186 		}
187 
188 		File dir = dst.getParentFile();
189 		if ((dir.exists() || !dir.mkdirs()) && !dir.isDirectory())
190 			return false;
191 		try {
192 			FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
193 			return true;
194 		} catch (IOException e) {
195 			LOG.error(e.getMessage(), e);
196 			return false;
197 		}
198 	}
199 
200 	private boolean linkHEAD(RefUpdate target) {
201 		try {
202 			RefUpdate u = refdb.newUpdate(Constants.HEAD, false);
203 			u.disableRefLog();
204 			switch (u.link(target.getName())) {
205 			case NEW:
206 			case FORCED:
207 			case NO_CHANGE:
208 				return true;
209 			default:
210 				return false;
211 			}
212 		} catch (IOException e) {
213 			return false;
214 		}
215 	}
216 }