path: optimize common dir checking

Instead of a linear search over common_list to check whether a path is common, use a trie. The trie search operates on path prefixes, and handles excludes. Signed-off-by: David Turner <dturner@twopensource.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

David Turner committed Aug 31, 2015 at 22:13 UTC 4e09cf2acf00c63848c365479c61d80af62eceba
2 files changed +214 -14
path.c
+213 -14
@@ -121,25 +121,224 @@ static struct common_dir common_list[] = {
121 { 0, 0, 0, NULL }
122 };
123
124 -static void update_common_dir(struct strbuf *buf, int git_dir_len)
124 +/*
125 + * A compressed trie. A trie node consists of zero or more characters that
126 + * are common to all elements with this prefix, optionally followed by some
127 + * children. If value is not NULL, the trie node is a terminal node.
128 + *
129 + * For example, consider the following set of strings:
130 + * abc
131 + * def
132 + * definite
133 + * definition
134 + *
135 + * The trie would look look like:
136 + * root: len = 0, children a and d non-NULL, value = NULL.
137 + * a: len = 2, contents = bc, value = (data for "abc")
138 + * d: len = 2, contents = ef, children i non-NULL, value = (data for "def")
139 + * i: len = 3, contents = nit, children e and i non-NULL, value = NULL
140 + * e: len = 0, children all NULL, value = (data for "definite")
141 + * i: len = 2, contents = on, children all NULL,
142 + * value = (data for "definition")
143 + */
144 +struct trie {
145 + struct trie *children[256];
146 + int len;
147 + char *contents;
148 + void *value;
149 +};
150 +
151 +static struct trie *make_trie_node(const char *key, void *value)
152 {
126 - char *base = buf->buf + git_dir_len;
127 - const struct common_dir *p;
153 + struct trie *new_node = xcalloc(1, sizeof(*new_node));
154 + new_node->len = strlen(key);
155 + if (new_node->len) {
156 + new_node->contents = xmalloc(new_node->len);
157 + memcpy(new_node->contents, key, new_node->len);
158 + }
159 + new_node->value = value;
160 + return new_node;
161 +}
162
129 - if (is_dir_file(base, "logs", "HEAD") ||
130 - is_dir_file(base, "info", "sparse-checkout"))
131 - return; /* keep this in $GIT_DIR */
132 - for (p = common_list; p->dirname; p++) {
133 - const char *path = p->dirname;
134 - if (p->is_dir && dir_prefix(base, path)) {
135 - replace_dir(buf, git_dir_len, get_git_common_dir());
136 - return;
163 +/*
164 + * Add a key/value pair to a trie. The key is assumed to be \0-terminated.
165 + * If there was an existing value for this key, return it.
166 + */
167 +static void *add_to_trie(struct trie *root, const char *key, void *value)
168 +{
169 + struct trie *child;
170 + void *old;
171 + int i;
172 +
173 + if (!*key) {
174 + /* we have reached the end of the key */
175 + old = root->value;
176 + root->value = value;
177 + return old;
178 + }
179 +
180 + for (i = 0; i < root->len; i++) {
181 + if (root->contents[i] == key[i])
182 + continue;
183 +
184 + /*
185 + * Split this node: child will contain this node's
186 + * existing children.
187 + */
188 + child = malloc(sizeof(*child));
189 + memcpy(child->children, root->children, sizeof(root->children));
190 +
191 + child->len = root->len - i - 1;
192 + if (child->len) {
193 + child->contents = xstrndup(root->contents + i + 1,
194 + child->len);
195 }
138 - if (!p->is_dir && !strcmp(base, path)) {
139 - replace_dir(buf, git_dir_len, get_git_common_dir());
140 - return;
196 + child->value = root->value;
197 + root->value = NULL;
198 + root->len = i;
199 +
200 + memset(root->children, 0, sizeof(root->children));
201 + root->children[(unsigned char)root->contents[i]] = child;
202 +
203 + /* This is the newly-added child. */
204 + root->children[(unsigned char)key[i]] =
205 + make_trie_node(key + i + 1, value);
206 + return NULL;
207 + }
208 +
209 + /* We have matched the entire compressed section */
210 + if (key[i]) {
211 + child = root->children[(unsigned char)key[root->len]];
212 + if (child) {
213 + return add_to_trie(child, key + root->len + 1, value);
214 + } else {
215 + child = make_trie_node(key + root->len + 1, value);
216 + root->children[(unsigned char)key[root->len]] = child;
217 + return NULL;
218 }
219 }
220 +
221 + old = root->value;
222 + root->value = value;
223 + return old;
224 +}
225 +
226 +typedef int (*match_fn)(const char *unmatched, void *data, void *baton);
227 +
228 +/*
229 + * Search a trie for some key. Find the longest /-or-\0-terminated
230 + * prefix of the key for which the trie contains a value. Call fn
231 + * with the unmatched portion of the key and the found value, and
232 + * return its return value. If there is no such prefix, return -1.
233 + *
234 + * The key is partially normalized: consecutive slashes are skipped.
235 + *
236 + * For example, consider the trie containing only [refs,
237 + * refs/worktree] (both with values).
238 + *
239 + * | key | unmatched | val from node | return value |
240 + * |-----------------|------------|---------------|--------------|
241 + * | a | not called | n/a | -1 |
242 + * | refs | \0 | refs | as per fn |
243 + * | refs/ | / | refs | as per fn |
244 + * | refs/w | /w | refs | as per fn |
245 + * | refs/worktree | \0 | refs/worktree | as per fn |
246 + * | refs/worktree/ | / | refs/worktree | as per fn |
247 + * | refs/worktree/a | /a | refs/worktree | as per fn |
248 + * |-----------------|------------|---------------|--------------|
249 + *
250 + */
251 +static int trie_find(struct trie *root, const char *key, match_fn fn,
252 + void *baton)
253 +{
254 + int i;
255 + int result;
256 + struct trie *child;
257 +
258 + if (!*key) {
259 + /* we have reached the end of the key */
260 + if (root->value && !root->len)
261 + return fn(key, root->value, baton);
262 + else
263 + return -1;
264 + }
265 +
266 + for (i = 0; i < root->len; i++) {
267 + /* Partial path normalization: skip consecutive slashes. */
268 + if (key[i] == '/' && key[i+1] == '/') {
269 + key++;
270 + continue;
271 + }
272 + if (root->contents[i] != key[i])
273 + return -1;
274 + }
275 +
276 + /* Matched the entire compressed section */
277 + key += i;
278 + if (!*key)
279 + /* End of key */
280 + return fn(key, root->value, baton);
281 +
282 + /* Partial path normalization: skip consecutive slashes */
283 + while (key[0] == '/' && key[1] == '/')
284 + key++;
285 +
286 + child = root->children[(unsigned char)*key];
287 + if (child)
288 + result = trie_find(child, key + 1, fn, baton);
289 + else
290 + result = -1;
291 +
292 + if (result >= 0 || (*key != '/' && *key != 0))
293 + return result;
294 + if (root->value)
295 + return fn(key, root->value, baton);
296 + else
297 + return -1;
298 +}
299 +
300 +static struct trie common_trie;
301 +static int common_trie_done_setup;
302 +
303 +static void init_common_trie(void)
304 +{
305 + struct common_dir *p;
306 +
307 + if (common_trie_done_setup)
308 + return;
309 +
310 + for (p = common_list; p->dirname; p++)
311 + add_to_trie(&common_trie, p->dirname, p);
312 +
313 + common_trie_done_setup = 1;
314 +}
315 +
316 +/*
317 + * Helper function for update_common_dir: returns 1 if the dir
318 + * prefix is common.
319 + */
320 +static int check_common(const char *unmatched, void *value, void *baton)
321 +{
322 + struct common_dir *dir = value;
323 +
324 + if (!dir)
325 + return 0;
326 +
327 + if (dir->is_dir && (unmatched[0] == 0 || unmatched[0] == '/'))
328 + return !dir->exclude;
329 +
330 + if (!dir->is_dir && unmatched[0] == 0)
331 + return !dir->exclude;
332 +
333 + return 0;
334 +}
335 +
336 +static void update_common_dir(struct strbuf *buf, int git_dir_len)
337 +{
338 + char *base = buf->buf + git_dir_len;
339 + init_common_trie();
340 + if (trie_find(&common_trie, base, check_common, NULL) > 0)
341 + replace_dir(buf, git_dir_len, get_git_common_dir());
342 }
343
344 void report_linked_checkout_garbage(void)
t/t0060-path-utils.sh
+1
@@ -271,6 +271,7 @@ test_git_path GIT_COMMON_DIR=bar objects/bar bar/objects/bar
271 test_git_path GIT_COMMON_DIR=bar info/exclude bar/info/exclude
272 test_git_path GIT_COMMON_DIR=bar info/grafts bar/info/grafts
273 test_git_path GIT_COMMON_DIR=bar info/sparse-checkout .git/info/sparse-checkout
274 +test_git_path GIT_COMMON_DIR=bar info//sparse-checkout .git/info//sparse-checkout
275 test_git_path GIT_COMMON_DIR=bar remotes/bar bar/remotes/bar
276 test_git_path GIT_COMMON_DIR=bar branches/bar bar/branches/bar
277 test_git_path GIT_COMMON_DIR=bar logs/refs/heads/master bar/logs/refs/heads/master