/* * 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] Error checking path %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; } static int apply_gitconfig(const char *key, const char *value, const __attribute__((unused)) struct config_context *cfg_ctx, void *cb) { const char *name; 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)) cgit_repo_config(current_repo, name, value); return 0; } static void apply_cgitrc(const char *name, const char *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] Error accessing %s: %s (%d)\n", path->buf, strerror(errno), errno); return; } strbuf_addch(path, '/'); pathlen = path->len; if (ctx.cfg.strict_export) { strbuf_addstr(path, ctx.cfg.strict_export); if (stat(path->buf, &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); current_repo = cgit_add_repo(relpath.buf); if (ctx.cfg.enable_git_config) { strbuf_addstr(path, "config"); git_config_from_file(apply_gitconfig, path->buf, NULL); 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'; } current_repo->path = cgit_strdup_first_line(path->buf); while (!current_repo->owner) { if ((pwd = getpwuid(st.st_uid)) == NULL) { fprintf(stderr, "[cgit] Error reading owner-info for %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) if ((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; } static void scan_path(const char *base, const char *path) { DIR *dir = opendir(path); struct dirent *ent; struct strbuf pathbuf = STRBUF_INIT; size_t pathlen = strlen(path); struct stat st; if (!dir) { fprintf(stderr, "[cgit] Error opening directory %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)) != NULL) { if (!should_scan(ent)) continue; strbuf_setlen(&pathbuf, pathlen); strbuf_addstr(&pathbuf, ent->d_name); if (stat(pathbuf.buf, &st)) { fprintf(stderr, "[cgit] Error checking path %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] Error opening projectsfile %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] Error reading from projectsfile %s: %s (%d)\n", projectsfile, strerror(err), err); } fclose(projects); strbuf_release(&line); } void scan_tree(const char *path) { scan_path(path, path); }