/* * Discovery of repositories on disk, so that an administrator can point cgit * at a directory instead of naming every repository in cgitrc. The tree below * that directory is walked for bare repositories and working trees, or when a * project list is configured only the paths it names are visited. Every * repository found is registered under its path relative to the root of the * scan, with owner, description and section taken from the files git keeps * beside it, and a cgitrc in the repository has the last word over all of them. */ #include "cgit.h" #include "config.h" #include "scan-tree.h" #include "shared.h" // Git writes this line into the description file of every repository it // creates, so a repository still carrying it gets cgit's own default instead. static const char *default_git_desc = "Unnamed repository; edit this file 'description' to name the repository."; // The config callbacks are handed only a name and a value, so the repository // being filled in waits here for the length of one add_repo call. static struct cgit_repo *current_repo; static int stat_entry(const char *dir, const char *name, struct stat *st) { struct strbuf path = STRBUF_INIT; int err; strbuf_addf(&path, "%s/%s", dir, name); err = stat(path.buf, st); // A missing entry is the ordinary answer for a directory that is not a // repository, so only some other failure is worth reporting. if (err && errno != ENOENT) fprintf(stderr, "[cgit] Unable to stat %s: %s (%d)\n", dir, strerror(errno), errno); strbuf_release(&path); return err; } static int is_git_dir(const char *path) { struct stat st; if (stat_entry(path, "objects", &st) || !S_ISDIR(st.st_mode)) return 0; if (stat_entry(path, "HEAD", &st) || !S_ISREG(st.st_mode)) return 0; return 1; } /* * A repository's own files belong to whoever can push to it, so the keys * that run a command, put raw markup on the page, hand a file to the browser * as markup, read a file off the disk or place a link wait on * trust-scan-config. A readme naming a git object, the form with a colon, * only reads from the repository and passes. */ static int trusted_key(const char *name, const char *value) { int untrusted; if (ctx.cfg.trust_scan_config) return 1; untrusted = ends_with(name, "-filter") || !strcmp(name, "head-content") || !strcmp(name, "module-link") || starts_with(name, "module-link.") || !strcmp(name, "logo") || !strcmp(name, "logo-link") || !strcmp(name, "clone-url") || !strcmp(name, "enable-html-serving") || (!strcmp(name, "readme") && !strchr(value, ':')); if (!untrusted) return 1; fprintf(stderr, "[cgit] Ignoring %s in %s: trust-scan-config is not set\n", name, current_repo->path); return 0; } static int apply_gitconfig(const char *key, const char *value, const __attribute__((unused)) struct config_context *cfg_ctx, void *cb) { const char *name; // A key with no value is legal git config and nothing here can use it. if (!value) return 0; if (!strcmp(key, "gitweb.owner")) cgit_repo_config(current_repo, "owner", value); else if (!strcmp(key, "gitweb.description")) cgit_repo_config(current_repo, "desc", value); else if (!strcmp(key, "gitweb.category")) cgit_repo_config(current_repo, "section", value); else if (skip_prefix(key, "cgit.", &name) && trusted_key(name, value)) cgit_repo_config(current_repo, name, value); return 0; } static void apply_cgitrc(const char *name, const char *value) { if (trusted_key(name, value)) cgit_repo_config(current_repo, name, value); } static char *find_char_back(char *start, char *from, int c) { while (from >= start && *from != c) from--; return from < start ? NULL : from; } /* * A positive depth counts separators from the left of the path and a negative * one counts back from the right. */ static char *section_slash(struct strbuf *relpath, int depth) { char *slash; if (depth > 0) { slash = relpath->buf - 1; while (depth && (slash = strchr(slash + 1, '/'))) depth--; } else { slash = relpath->buf + relpath->len; while (slash && depth && (slash = find_char_back(relpath->buf, slash - 1, '/'))) depth++; } return slash && !depth ? slash : NULL; } static void set_section_from_path(struct strbuf *relpath, int depth) { char *slash = section_slash(relpath, depth); if (!slash) return; *slash = '\0'; current_repo->section = cgit_strdup_first_line(relpath->buf); *slash = '/'; if (starts_with(current_repo->name, current_repo->section)) { current_repo->name += strlen(current_repo->section); if (*current_repo->name == '/') current_repo->name++; } } static void add_repo(const char *base, struct strbuf *path) { struct stat st; struct passwd *pwd; size_t pathlen; struct strbuf relpath = STRBUF_INIT; char *comma; size_t desc_size; if (stat(path->buf, &st)) { fprintf(stderr, "[cgit] Unable to access %s: %s (%d)\n", path->buf, strerror(errno), errno); return; } strbuf_addch(path, '/'); pathlen = path->len; if (ctx.cfg.strict_export) { struct stat export_st; strbuf_addstr(path, ctx.cfg.strict_export); if (stat(path->buf, &export_st)) return; strbuf_setlen(path, pathlen); } strbuf_addstr(path, "noweb"); if (!stat(path->buf, &st)) return; strbuf_setlen(path, pathlen); if (!starts_with(path->buf, base)) strbuf_addbuf(&relpath, path); else strbuf_addstr(&relpath, path->buf + strlen(base) + 1); // Drop the trailing slash added above, or the "/.git" suffix test // below never matches and a working tree is named "repo/.git". if (relpath.len && relpath.buf[relpath.len - 1] == '/') strbuf_setlen(&relpath, relpath.len - 1); if (relpath.len >= 5 && !strcmp(relpath.buf + relpath.len - 5, "/.git")) strbuf_setlen(&relpath, relpath.len - 5); else if (!strcmp(relpath.buf, ".git")) strbuf_setlen(&relpath, 0); // The scan root may itself be the repository, leaving nothing after // the base, so that one is named after its directory. if (!relpath.len) { const char *end = path->buf + pathlen - 1, *start; if (end - path->buf >= 5 && !strncmp(end - 5, "/.git", 5)) end -= 5; start = end; while (start > path->buf && start[-1] != '/') start--; strbuf_add(&relpath, start, end - start); } current_repo = cgit_add_repo(relpath.buf); // Set before the config files are read, since a warning about a key // found in them names the repository by this path. current_repo->path = cgit_strdup_first_line(path->buf); if (ctx.cfg.enable_git_config) { // A pusher wrote this file, so a broken one is skipped with a // warning rather than allowed to end the request. struct config_options opts = { .error_action = CONFIG_ERROR_SILENT }; strbuf_addstr(path, "config"); if (git_config_from_file_with_options(apply_gitconfig, path->buf, NULL, CONFIG_SCOPE_UNKNOWN, &opts)) fprintf(stderr, "[cgit] Ignoring unreadable config in %s\n", path->buf); strbuf_setlen(path, pathlen); } if (ctx.cfg.remove_suffix) { size_t urllen; strip_suffix(current_repo->url, ".git", &urllen); strip_suffix_mem(current_repo->url, &urllen, "/"); current_repo->url[urllen] = '\0'; } while (!current_repo->owner) { if (!(pwd = getpwuid(st.st_uid))) { fprintf(stderr, "[cgit] Unable to read the owner of %s: %s (%d)\n", path->buf, strerror(errno), errno); break; } // A gecos field puts the owner's name in front of a comma // separated list of office and phone details. if (pwd->pw_gecos && (comma = strchr(pwd->pw_gecos, ','))) *comma = '\0'; current_repo->owner = cgit_strdup_first_line(pwd->pw_gecos ? pwd->pw_gecos : pwd->pw_name); } if (current_repo->desc == cgit_default_repo_desc || !current_repo->desc) { strbuf_addstr(path, "description"); if (!stat(path->buf, &st)) cgit_read_first_line(path->buf, ¤t_repo->desc, &desc_size); strbuf_setlen(path, pathlen); if (current_repo->desc && !strcmp(current_repo->desc, default_git_desc)) { free(current_repo->desc); current_repo->desc = cgit_default_repo_desc; } } if (ctx.cfg.section_from_path) set_section_from_path(&relpath, ctx.cfg.section_from_path); strbuf_addstr(path, "cgitrc"); if (!stat(path->buf, &st)) config_file_parse(path->buf, apply_cgitrc); strbuf_release(&relpath); } static int should_scan(const struct dirent *ent) { if (ent->d_name[0] != '.') return 1; if (ent->d_name[1] == '\0') return 0; if (ent->d_name[1] == '.' && ent->d_name[2] == '\0') return 0; return ctx.cfg.scan_hidden_path; } // Symlinks are followed so that a link into the scan path counts, which means // a link back at an ancestor would recurse until the path ran out of room. // Each directory is entered once. static struct { dev_t dev; ino_t ino; } *visited; static size_t visited_nr, visited_alloc; static int already_visited(const char *path) { struct stat st; size_t i; if (stat(path, &st)) return 0; for (i = 0; i < visited_nr; i++) if (visited[i].dev == st.st_dev && visited[i].ino == st.st_ino) return 1; ALLOC_GROW(visited, visited_nr + 1, visited_alloc); visited[visited_nr].dev = st.st_dev; visited[visited_nr].ino = st.st_ino; visited_nr++; return 0; } static void scan_path(const char *base, const char *path) { DIR *dir; struct dirent *ent; struct strbuf pathbuf = STRBUF_INIT; size_t pathlen = strlen(path); struct stat st; if (already_visited(path)) return; dir = opendir(path); if (!dir) { fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", path, strerror(errno), errno); return; } strbuf_add(&pathbuf, path, pathlen); if (is_git_dir(pathbuf.buf)) { add_repo(base, &pathbuf); goto end; } strbuf_addstr(&pathbuf, "/.git"); if (is_git_dir(pathbuf.buf)) { add_repo(base, &pathbuf); goto end; } // Take in the '/' that "/.git" left in the buffer, since the loop below // truncates to this length and then appends an entry name straight on. pathlen++; while ((ent = readdir(dir))) { if (!should_scan(ent)) continue; strbuf_setlen(&pathbuf, pathlen); strbuf_addstr(&pathbuf, ent->d_name); if (stat(pathbuf.buf, &st)) { fprintf(stderr, "[cgit] Unable to stat %s: %s (%d)\n", pathbuf.buf, strerror(errno), errno); continue; } if (S_ISDIR(st.st_mode)) scan_path(base, pathbuf.buf); } end: strbuf_release(&pathbuf); closedir(dir); } void scan_projects(const char *path, const char *projectsfile) { struct strbuf line = STRBUF_INIT; FILE *projects; int err; projects = fopen(projectsfile, "r"); if (!projects) { fprintf(stderr, "[cgit] Unable to open project list %s: %s (%d)\n", projectsfile, strerror(errno), errno); return; } while (strbuf_getline(&line, projects) != EOF) { if (!line.len) continue; strbuf_insert(&line, 0, "/", 1); strbuf_insert(&line, 0, path, strlen(path)); scan_path(path, line.buf); } if ((err = ferror(projects))) { fprintf(stderr, "[cgit] Unable to read project list %s: %s (%d)\n", projectsfile, strerror(err), err); } fclose(projects); strbuf_release(&line); } void scan_tree(const char *path) { scan_path(path, path); }