forked from google/copybara
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAddExcludedFilesToIndex.java
More file actions
274 lines (243 loc) · 8.97 KB
/
AddExcludedFilesToIndex.java
File metadata and controls
274 lines (243 loc) · 8.97 KB
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
/*
* Copyright (C) 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.copybara.git;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.copybara.exception.RepoException;
import com.google.copybara.util.console.Console;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
/**
* A walker which adds all files not matching a glob to the index of a Git repo using {@code git
* add}.
*/
final class AddExcludedFilesToIndex {
private final GitRepository repo;
private final PathMatcher pathMatcher;
private ArrayList<String> addBackSubmodules;
AddExcludedFilesToIndex(GitRepository repo, PathMatcher pathMatcher) {
this.repo = Preconditions.checkNotNull(repo);
this.pathMatcher = Preconditions.checkNotNull(pathMatcher);
}
/**
* Finds and records the path of all submodules. This should be called when they are not staged
* for deletion.
*/
void findSubmodules(Console console) throws RepoException {
addBackSubmodules = new ArrayList<>();
String submoduleStatus = repo.simpleCommand("submodule", "status").getStdout();
for (String line : Splitter.on('\n').omitEmptyStrings().split(submoduleStatus)) {
String submoduleName = line.replaceFirst("^-[0-9a-f]{40} ", "");
if (submoduleName.equals(line)) {
console.warn("Cannot parse line from 'git submodule status': " + line);
continue;
}
if (!pathMatcher.matches(repo.getWorkTree().resolve(submoduleName))) {
addBackSubmodules.add(submoduleName);
}
}
}
/**
* Adds all the excluded files and submodules.
*/
void add(Console console) throws RepoException, IOException {
console.progress("Git Destination: Walking Tree for Exclusions");
ExcludesFinder visitor = new ExcludesFinder(repo.getGitDir(), pathMatcher);
Files.walkFileTree(repo.getWorkTree(), visitor);
console.progress("Git Destination: Compressing Tree");
visitor.excludedTree.Compress();
console.progress("Git Destination: Adding Excluded Files");
int size = 0;
List<String> current = new ArrayList<>();
for (String path : visitor.excludedTree.Excluded()) {
current.add(path);
size += path.length();
// Split the executions in chunks of 6K. 8K triggers arg max in some systems, so
// this is a reasonable number to get some batching benefit.
if (size > 6 * 1024) {
repo.add().force().files(current).run();
current = new ArrayList<>();
size = 0;
}
}
if (!current.isEmpty()) {
repo.add().force().files(current).run();
}
console.progress("Git Destination: Adding submodules");
for (String addBackSubmodule : addBackSubmodules) {
repo.simpleCommand("reset", "--", "--quiet", addBackSubmodule);
repo.add().force().files(ImmutableList.of(addBackSubmodule)).run();
}
}
/**
* A tree representation of the set of paths in the repo.
*
* Used to track which paths we're excluding, or equivalently the set of paths we're
* going to `git add` above.
*
* Each interior node is a directory, and each leaf is a path (either a directory or
* a file), with the leaves marked as included or not
*/
private static final class PathTree {
/**
* Internal representation of each leaf of the tree - the last component of its
* path, and whether it's included or not
*/
private static final class PathTreeLeaf {
private final String filename;
private boolean included;
private PathTreeLeaf(String filename, boolean included) {
this.filename = filename;
this.included = included;
}
}
private String dirname;
private final ArrayList<PathTree> kids;
private final ArrayList<PathTreeLeaf> leaves;
public PathTree(String dirname) {
this.dirname = dirname;
this.kids = new ArrayList<>();
this.leaves = new ArrayList<>();
}
/**
* Add a new path to the tree.
*
* The new path will be added as a leaf node, and marked as included/excluded.
*/
public void AddPath(Path path, boolean included) {
// Set the root dirname lazily when we get the first path
if (dirname == null) {
dirname = path.getRoot().toString();
}
// Walk down the tree from the root of the tree (and the root of the path)
PathTree currTree = this;
for (Path component : path.getParent()) {
// Do we already have an entry for this subdirectory?
boolean foundKid = false;
for (PathTree kid : currTree.kids) {
if (kid.dirname.equals(component.toString())) {
currTree = kid;
foundKid = true;
break;
}
}
// We don't, so create it
if (!foundKid) {
PathTree newTree = new PathTree(component.toString());
currTree.kids.add(newTree);
currTree = newTree;
}
}
// Now add the filename to the bottom subdirectory
currTree.leaves.add(new PathTreeLeaf(path.getFileName().toString(), included));
}
/**
* Compresses the tree.
*
* This takes subtrees where all files are excluded and replaces them with a single
* entry for the root of the subtree. This means when we go to call `git add`, we
* can just pass the root of the subdirectory, instead of every file inside it.
*
* This is done bottom-up, recursively. Each directory that has no complex
* subdirectories under it, and where all files are excluded, can be deleted from
* its parent's `kids` list and put instead in its `leaves` list as a single entry
* for the directory. We go bottom-up, so that entire trees can be replaced this
* way.
*
* Returns `true` if the PathTree can be compressed to a single entry
*/
public boolean Compress() {
Iterator<PathTree> itr = kids.iterator();
while (itr.hasNext()) {
PathTree subTree = itr.next();
boolean compressed = subTree.Compress();
if (compressed) {
leaves.add(new PathTreeLeaf(subTree.dirname, false));
itr.remove();
}
}
if (!kids.isEmpty()) {
return false;
}
for (PathTreeLeaf leaf : leaves) {
if (leaf.included) {
return false;
}
}
return true;
}
/**
* Helper to recursively add the excluded paths in this tree to the list `results`.
*
* Each path is prepended with the string `prefix`, i.e. the path from the root to
* this PathTree
*/
private void AddExcluded(ArrayList<String> results, String prefix) {
for (PathTree kid : kids) {
String childPath = prefix == null ? dirname : Paths.get(prefix, dirname).toString();
kid.AddExcluded(results, childPath);
}
for (PathTreeLeaf leaf : leaves) {
if (!leaf.included) {
String childPath = prefix == null
? Paths.get(dirname, leaf.filename).toString()
: Paths.get(prefix, dirname, leaf.filename).toString();
results.add(childPath);
}
}
}
/**
* Return the list of excluded paths in this tree.
*/
public ArrayList<String> Excluded() {
ArrayList<String> result = new ArrayList<>();
AddExcluded(result, dirname);
return result;
}
}
private static final class ExcludesFinder extends SimpleFileVisitor<Path> {
private final Path gitDir;
private final PathMatcher destinationFiles;
private final PathTree excludedTree = new PathTree(null);
private ExcludesFinder(Path gitDir, PathMatcher destinationFiles) {
this.gitDir = gitDir;
this.destinationFiles = destinationFiles;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
if (dir.equals(gitDir)) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
excludedTree.AddPath(file, destinationFiles.matches(file));
return FileVisitResult.CONTINUE;
}
}
}