1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
|
/*
* Copyright (C) 2017, Two Sigma Open Source and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.api;
import static org.eclipse.jgit.util.FileUtils.RECURSIVE;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidConfigurationException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.submodule.SubmoduleWalk;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FileUtils;
/**
* A class used to execute a submodule deinit command.
* <p>
* This will remove the module(s) from the working tree, but won't affect
* .git/modules.
*
* @since 4.10
* @see <a href=
* "http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html"
* >Git documentation about submodules</a>
*/
public class SubmoduleDeinitCommand
extends GitCommand<Collection<SubmoduleDeinitResult>> {
private final Collection<String> paths;
private boolean force;
/**
* Constructor of SubmoduleDeinitCommand
*
* @param repo
*/
public SubmoduleDeinitCommand(Repository repo) {
super(repo);
paths = new ArrayList<>();
}
/**
* {@inheritDoc}
* <p>
*
* @return the set of repositories successfully deinitialized.
* @throws NoSuchSubmoduleException
* if any of the submodules which we might want to deinitialize
* don't exist
*/
@Override
public Collection<SubmoduleDeinitResult> call() throws GitAPIException {
checkCallable();
try {
if (paths.isEmpty()) {
return Collections.emptyList();
}
for (String path : paths) {
if (!submoduleExists(path)) {
throw new NoSuchSubmoduleException(path);
}
}
List<SubmoduleDeinitResult> results = new ArrayList<>(paths.size());
try (RevWalk revWalk = new RevWalk(repo);
SubmoduleWalk generator = SubmoduleWalk.forIndex(repo)) {
generator.setFilter(PathFilterGroup.createFromStrings(paths));
StoredConfig config = repo.getConfig();
while (generator.next()) {
String path = generator.getPath();
String name = generator.getModuleName();
SubmoduleDeinitStatus status = checkDirty(revWalk, path);
switch (status) {
case SUCCESS:
deinit(path);
break;
case ALREADY_DEINITIALIZED:
break;
case DIRTY:
if (force) {
deinit(path);
status = SubmoduleDeinitStatus.FORCED;
}
break;
default:
throw new JGitInternalException(MessageFormat.format(
JGitText.get().unexpectedSubmoduleStatus,
status));
}
config.unsetSection(
ConfigConstants.CONFIG_SUBMODULE_SECTION, name);
results.add(new SubmoduleDeinitResult(path, status));
}
}
return results;
} catch (ConfigInvalidException e) {
throw new InvalidConfigurationException(e.getMessage(), e);
} catch (IOException e) {
throw new JGitInternalException(e.getMessage(), e);
}
}
/**
* Recursively delete the *contents* of path, but leave path as an empty
* directory
*
* @param path
* the path to clean
* @throws IOException
*/
private void deinit(String path) throws IOException {
File dir = new File(repo.getWorkTree(), path);
if (!dir.isDirectory()) {
throw new JGitInternalException(MessageFormat.format(
JGitText.get().expectedDirectoryNotSubmodule, path));
}
final File[] ls = dir.listFiles();
if (ls != null) {
for (File f : ls) {
FileUtils.delete(f, RECURSIVE);
}
}
}
/**
* Check if a submodule is dirty. A submodule is dirty if there are local
* changes to the submodule relative to its HEAD, including untracked files.
* It is also dirty if the HEAD of the submodule does not match the value in
* the parent repo's index or HEAD.
*
* @param revWalk
* @param path
* @return status of the command
* @throws GitAPIException
* @throws IOException
*/
private SubmoduleDeinitStatus checkDirty(RevWalk revWalk, String path)
throws GitAPIException, IOException {
Ref head = repo.exactRef("HEAD"); //$NON-NLS-1$
if (head == null) {
throw new NoHeadException(
JGitText.get().invalidRepositoryStateNoHead);
}
RevCommit headCommit = revWalk.parseCommit(head.getObjectId());
RevTree tree = headCommit.getTree();
ObjectId submoduleHead;
try (SubmoduleWalk w = SubmoduleWalk.forPath(repo, tree, path)) {
submoduleHead = w.getHead();
if (submoduleHead == null) {
// The submodule is not checked out.
return SubmoduleDeinitStatus.ALREADY_DEINITIALIZED;
}
if (!submoduleHead.equals(w.getObjectId())) {
// The submodule's current HEAD doesn't match the value in the
// outer repo's HEAD.
return SubmoduleDeinitStatus.DIRTY;
}
}
try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) {
if (!w.next()) {
// The submodule does not exist in the index (shouldn't happen
// since we check this earlier)
return SubmoduleDeinitStatus.DIRTY;
}
if (!submoduleHead.equals(w.getObjectId())) {
// The submodule's current HEAD doesn't match the value in the
// outer repo's index.
return SubmoduleDeinitStatus.DIRTY;
}
try (Repository submoduleRepo = w.getRepository()) {
Status status = Git.wrap(submoduleRepo).status().call();
return status.isClean() ? SubmoduleDeinitStatus.SUCCESS
: SubmoduleDeinitStatus.DIRTY;
}
}
}
/**
* Check if this path is a submodule by checking the index, which is what
* git submodule deinit checks.
*
* @param path
* path of the submodule
*
* @return {@code true} if path exists and is a submodule in index,
* {@code false} otherwise
* @throws IOException
*/
private boolean submoduleExists(String path) throws IOException {
TreeFilter filter = PathFilter.create(path);
try (SubmoduleWalk w = SubmoduleWalk.forIndex(repo)) {
return w.setFilter(filter).next();
}
}
/**
* Add repository-relative submodule path to deinitialize
*
* @param path
* (with <code>/</code> as separator)
* @return this command
*/
public SubmoduleDeinitCommand addPath(String path) {
paths.add(path);
return this;
}
/**
* If {@code true}, call() will deinitialize modules with local changes;
* else it will refuse to do so.
*
* @param force
* @return {@code this}
*/
public SubmoduleDeinitCommand setForce(boolean force) {
this.force = force;
return this;
}
/**
* The user tried to deinitialize a submodule that doesn't exist in the
* index.
*/
public static class NoSuchSubmoduleException extends GitAPIException {
private static final long serialVersionUID = 1L;
/**
* Constructor of NoSuchSubmoduleException
*
* @param path
* path of non-existing submodule
*/
public NoSuchSubmoduleException(String path) {
super(MessageFormat.format(JGitText.get().noSuchSubmodule, path));
}
}
/**
* The effect of a submodule deinit command for a given path
*/
public enum SubmoduleDeinitStatus {
/**
* The submodule was not initialized in the first place
*/
ALREADY_DEINITIALIZED,
/**
* The submodule was deinitialized
*/
SUCCESS,
/**
* The submodule had local changes, but was deinitialized successfully
*/
FORCED,
/**
* The submodule had local changes and force was false
*/
DIRTY,
}
}
|