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  	/** A reference we backup {@link #objId} into during the rename. */
55  	private RefDirectoryUpdate tmp;
56  
57  	RefDirectoryRename(RefDirectoryUpdate src, RefDirectoryUpdate dst) {
58  		super(src, dst);
59  		refdb = src.getRefDatabase();
60  	}
61  
62  	/** {@inheritDoc} */
63  	@Override
64  	protected Result doRename() throws IOException {
65  		if (source.getRef().isSymbolic())
66  			return Result.IO_FAILURE; // not supported
67  
68  		objId = source.getOldObjectId();
69  		boolean updateHEAD = needToUpdateHEAD();
70  		tmp = refdb.newTemporaryUpdate();
71  		try (RevWalk rw = new RevWalk(refdb.getRepository())) {
72  			// First backup the source so its never unreachable.
73  			tmp.setNewObjectId(objId);
74  			tmp.setForceUpdate(true);
75  			tmp.disableRefLog();
76  			switch (tmp.update(rw)) {
77  			case NEW:
78  			case FORCED:
79  			case NO_CHANGE:
80  				break;
81  			default:
82  				return tmp.getResult();
83  			}
84  
85  			// Save the source's log under the temporary name, we must do
86  			// this before we delete the source, otherwise we lose the log.
87  			if (!renameLog(source, tmp))
88  				return Result.IO_FAILURE;
89  
90  			// If HEAD has to be updated, link it now to destination.
91  			// We have to link before we delete, otherwise the delete
92  			// fails because its the current branch.
93  			RefUpdate dst = destination;
94  			if (updateHEAD) {
95  				if (!linkHEAD(destination)) {
96  					renameLog(tmp, source);
97  					return Result.LOCK_FAILURE;
98  				}
99  
100 				// Replace the update operation so HEAD will log the rename.
101 				dst = refdb.newUpdate(Constants.HEAD, false);
102 				dst.setRefLogIdent(destination.getRefLogIdent());
103 				dst.setRefLogMessage(destination.getRefLogMessage(), false);
104 			}
105 
106 			// Delete the source name so its path is free for replacement.
107 			source.setExpectedOldObjectId(objId);
108 			source.setForceUpdate(true);
109 			source.disableRefLog();
110 			if (source.delete(rw) != Result.FORCED) {
111 				renameLog(tmp, source);
112 				if (updateHEAD)
113 					linkHEAD(source);
114 				return source.getResult();
115 			}
116 
117 			// Move the log to the destination.
118 			if (!renameLog(tmp, destination)) {
119 				renameLog(tmp, source);
120 				source.setExpectedOldObjectId(ObjectId.zeroId());
121 				source.setNewObjectId(objId);
122 				source.update(rw);
123 				if (updateHEAD)
124 					linkHEAD(source);
125 				return Result.IO_FAILURE;
126 			}
127 
128 			// Create the destination, logging the rename during the creation.
129 			dst.setExpectedOldObjectId(ObjectId.zeroId());
130 			dst.setNewObjectId(objId);
131 			if (dst.update(rw) != Result.NEW) {
132 				// If we didn't create the destination we have to undo
133 				// our work. Put the log back and restore source.
134 				if (renameLog(destination, tmp))
135 					renameLog(tmp, source);
136 				source.setExpectedOldObjectId(ObjectId.zeroId());
137 				source.setNewObjectId(objId);
138 				source.update(rw);
139 				if (updateHEAD)
140 					linkHEAD(source);
141 				return dst.getResult();
142 			}
143 
144 			return Result.RENAMED;
145 		} finally {
146 			// Always try to free the temporary name.
147 			try {
148 				refdb.delete(tmp);
149 			} catch (IOException err) {
150 				FileUtils.delete(refdb.fileFor(tmp.getName()));
151 			}
152 		}
153 	}
154 
155 	private boolean renameLog(RefUpdate src, RefUpdate dst) {
156 		File srcLog = refdb.logFor(src.getName());
157 		File dstLog = refdb.logFor(dst.getName());
158 
159 		if (!srcLog.exists())
160 			return true;
161 
162 		if (!rename(srcLog, dstLog))
163 			return false;
164 
165 		try {
166 			final int levels = RefDirectory.levelsIn(src.getName()) - 2;
167 			RefDirectory.delete(srcLog, levels);
168 			return true;
169 		} catch (IOException e) {
170 			rename(dstLog, srcLog);
171 			return false;
172 		}
173 	}
174 
175 	private static boolean rename(File src, File dst) {
176 		try {
177 			FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
178 			return true;
179 		} catch (AtomicMoveNotSupportedException e) {
180 			LOG.error(e.getMessage(), e);
181 		} catch (IOException e) {
182 			// ignore
183 		}
184 
185 		File dir = dst.getParentFile();
186 		if ((dir.exists() || !dir.mkdirs()) && !dir.isDirectory())
187 			return false;
188 		try {
189 			FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
190 			return true;
191 		} catch (IOException e) {
192 			LOG.error(e.getMessage(), e);
193 			return false;
194 		}
195 	}
196 
197 	private boolean linkHEAD(RefUpdate target) {
198 		try {
199 			RefUpdate u = refdb.newUpdate(Constants.HEAD, false);
200 			u.disableRefLog();
201 			switch (u.link(target.getName())) {
202 			case NEW:
203 			case FORCED:
204 			case NO_CHANGE:
205 				return true;
206 			default:
207 				return false;
208 			}
209 		} catch (IOException e) {
210 			return false;
211 		}
212 	}
213 }