Merge branch 'vd/scalar-generalize-diagnose'

The "diagnose" feature to create a zip archive for diagnostic
material has been lifted from "scalar" and made into a feature of
"git bugreport".

* vd/scalar-generalize-diagnose:
  scalar: update technical doc roadmap
  scalar-diagnose: use 'git diagnose --mode=all'
  builtin/bugreport.c: create '--diagnose' option
  builtin/diagnose.c: add '--mode' option
  builtin/diagnose.c: create 'git diagnose' builtin
  diagnose.c: add option to configure archive contents
  scalar-diagnose: move functionality to common location
  scalar-diagnose: move 'get_disk_info()' to 'compat/'
  scalar-diagnose: add directory to archiver more gently
  scalar-diagnose: avoid 32-bit overflow of size_t
  scalar-diagnose: use "$GIT_UNZIP" in test
This commit is contained in:
Junio C Hamano 2022-08-25 14:42:32 -07:00
commit f00ddc9f48
17 changed files with 637 additions and 278 deletions

1
.gitignore vendored
View File

@ -53,6 +53,7 @@
/git-cvsimport
/git-cvsserver
/git-daemon
/git-diagnose
/git-diff
/git-diff-files
/git-diff-index

View File

@ -9,6 +9,7 @@ SYNOPSIS
--------
[verse]
'git bugreport' [(-o | --output-directory) <path>] [(-s | --suffix) <format>]
[--diagnose[=<mode>]]
DESCRIPTION
-----------
@ -31,6 +32,10 @@ The following information is captured automatically:
- A list of enabled hooks
- $SHELL
Additional information may be gathered into a separate zip archive using the
`--diagnose` option, and can be attached alongside the bugreport document to
provide additional context to readers.
This tool is invoked via the typical Git setup process, which means that in some
cases, it might not be able to launch - for example, if a relevant config file
is unreadable. In this kind of scenario, it may be helpful to manually gather
@ -49,6 +54,19 @@ OPTIONS
named 'git-bugreport-<formatted suffix>'. This should take the form of a
strftime(3) format string; the current local time will be used.
--no-diagnose::
--diagnose[=<mode>]::
Create a zip archive of supplemental information about the user's
machine, Git client, and repository state. The archive is written to the
same output directory as the bug report and is named
'git-diagnostics-<formatted suffix>'.
+
Without `mode` specified, the diagnostic archive will contain the default set of
statistics reported by `git diagnose`. An optional `mode` value may be specified
to change which information is included in the archive. See
linkgit:git-diagnose[1] for the list of valid values for `mode` and details
about their usage.
GIT
---
Part of the linkgit:git[1] suite

View File

@ -0,0 +1,65 @@
git-diagnose(1)
================
NAME
----
git-diagnose - Generate a zip archive of diagnostic information
SYNOPSIS
--------
[verse]
'git diagnose' [(-o | --output-directory) <path>] [(-s | --suffix) <format>]
[--mode=<mode>]
DESCRIPTION
-----------
Collects detailed information about the user's machine, Git client, and
repository state and packages that information into a zip archive. The
generated archive can then, for example, be shared with the Git mailing list to
help debug an issue or serve as a reference for independent debugging.
By default, the following information is captured in the archive:
* 'git version --build-options'
* The path to the repository root
* The available disk space on the filesystem
* The name and size of each packfile, including those in alternate object
stores
* The total count of loose objects, as well as counts broken down by
`.git/objects` subdirectory
Additional information can be collected by selecting a different diagnostic mode
using the `--mode` option.
This tool differs from linkgit:git-bugreport[1] in that it collects much more
detailed information with a greater focus on reporting the size and data shape
of repository contents.
OPTIONS
-------
-o <path>::
--output-directory <path>::
Place the resulting diagnostics archive in `<path>` instead of the
current directory.
-s <format>::
--suffix <format>::
Specify an alternate suffix for the diagnostics archive name, to create
a file named 'git-diagnostics-<formatted suffix>'. This should take the
form of a strftime(3) format string; the current local time will be
used.
--mode=(stats|all)::
Specify the type of diagnostics that should be collected. The default behavior
of 'git diagnose' is equivalent to `--mode=stats`.
+
The `--mode=all` option collects everything included in `--mode=stats`, as well
as copies of `.git`, `.git/hooks`, `.git/info`, `.git/logs`, and
`.git/objects/info` directories. This additional information may be sensitive,
as it can be used to reconstruct the full contents of the diagnosed repository.
Users should exercise caution when sharing an archive generated with
`--mode=all`.
GIT
---
Part of the linkgit:git[1] suite

View File

@ -84,6 +84,9 @@ series have been accepted:
- `scalar-diagnose`: The `scalar` command is taught the `diagnose` subcommand.
- `scalar-generalize-diagnose`: Move the functionality of `scalar diagnose`
into `git diagnose` and `git bugreport --diagnose`.
Roughly speaking (and subject to change), the following series are needed to
"finish" this initial version of Scalar:
@ -91,12 +94,6 @@ Roughly speaking (and subject to change), the following series are needed to
and implement `scalar help`. At the end of this series, Scalar should be
feature-complete from the perspective of a user.
- Generalize features not specific to Scalar: In the spirit of making Scalar
configure only what is needed for large repo performance, move common
utilities into other parts of Git. Some of this will be internal-only, but one
major change will be generalizing `scalar diagnose` for use with any Git
repository.
- Move Scalar to toplevel: Move Scalar out of `contrib/` and into the root of
`git`, including updates to build and install it with the rest of Git. This
change will incorporate Scalar into the Git CI and test framework, as well as

View File

@ -933,6 +933,7 @@ LIB_OBJS += ctype.o
LIB_OBJS += date.o
LIB_OBJS += decorate.o
LIB_OBJS += delta-islands.o
LIB_OBJS += diagnose.o
LIB_OBJS += diff-delta.o
LIB_OBJS += diff-merges.o
LIB_OBJS += diff-lib.o
@ -1153,6 +1154,7 @@ BUILTIN_OBJS += builtin/credential-cache.o
BUILTIN_OBJS += builtin/credential-store.o
BUILTIN_OBJS += builtin/credential.o
BUILTIN_OBJS += builtin/describe.o
BUILTIN_OBJS += builtin/diagnose.o
BUILTIN_OBJS += builtin/diff-files.o
BUILTIN_OBJS += builtin/diff-index.o
BUILTIN_OBJS += builtin/diff-tree.o

View File

@ -144,6 +144,7 @@ int cmd_credential_cache(int argc, const char **argv, const char *prefix);
int cmd_credential_cache_daemon(int argc, const char **argv, const char *prefix);
int cmd_credential_store(int argc, const char **argv, const char *prefix);
int cmd_describe(int argc, const char **argv, const char *prefix);
int cmd_diagnose(int argc, const char **argv, const char *prefix);
int cmd_diff_files(int argc, const char **argv, const char *prefix);
int cmd_diff_index(int argc, const char **argv, const char *prefix);
int cmd_diff(int argc, const char **argv, const char *prefix);

View File

@ -5,6 +5,7 @@
#include "compat/compiler.h"
#include "hook.h"
#include "hook-list.h"
#include "diagnose.h"
static void get_system_info(struct strbuf *sys_info)
@ -59,7 +60,7 @@ static void get_populated_hooks(struct strbuf *hook_info, int nongit)
}
static const char * const bugreport_usage[] = {
N_("git bugreport [-o|--output-directory <file>] [-s|--suffix <format>]"),
N_("git bugreport [-o|--output-directory <file>] [-s|--suffix <format>] [--diagnose[=<mode>]"),
NULL
};
@ -98,16 +99,21 @@ int cmd_bugreport(int argc, const char **argv, const char *prefix)
int report = -1;
time_t now = time(NULL);
struct tm tm;
enum diagnose_mode diagnose = DIAGNOSE_NONE;
char *option_output = NULL;
char *option_suffix = "%Y-%m-%d-%H%M";
const char *user_relative_path = NULL;
char *prefixed_filename;
size_t output_path_len;
const struct option bugreport_options[] = {
OPT_CALLBACK_F(0, "diagnose", &diagnose, N_("mode"),
N_("create an additional zip archive of detailed diagnostics (default 'stats')"),
PARSE_OPT_OPTARG, option_parse_diagnose),
OPT_STRING('o', "output-directory", &option_output, N_("path"),
N_("specify a destination for the bugreport file")),
N_("specify a destination for the bugreport file(s)")),
OPT_STRING('s', "suffix", &option_suffix, N_("format"),
N_("specify a strftime format suffix for the filename")),
N_("specify a strftime format suffix for the filename(s)")),
OPT_END()
};
@ -119,6 +125,7 @@ int cmd_bugreport(int argc, const char **argv, const char *prefix)
option_output ? option_output : "");
strbuf_addstr(&report_path, prefixed_filename);
strbuf_complete(&report_path, '/');
output_path_len = report_path.len;
strbuf_addstr(&report_path, "git-bugreport-");
strbuf_addftime(&report_path, option_suffix, localtime_r(&now, &tm), 0, 0);
@ -133,6 +140,20 @@ int cmd_bugreport(int argc, const char **argv, const char *prefix)
report_path.buf);
}
/* Prepare diagnostics, if requested */
if (diagnose != DIAGNOSE_NONE) {
struct strbuf zip_path = STRBUF_INIT;
strbuf_add(&zip_path, report_path.buf, output_path_len);
strbuf_addstr(&zip_path, "git-diagnostics-");
strbuf_addftime(&zip_path, option_suffix, localtime_r(&now, &tm), 0, 0);
strbuf_addstr(&zip_path, ".zip");
if (create_diagnostics_archive(&zip_path, diagnose))
die_errno(_("unable to create diagnostics archive %s"), zip_path.buf);
strbuf_release(&zip_path);
}
/* Prepare the report contents */
get_bug_template(&buffer);

61
builtin/diagnose.c Normal file
View File

@ -0,0 +1,61 @@
#include "builtin.h"
#include "parse-options.h"
#include "diagnose.h"
static const char * const diagnose_usage[] = {
N_("git diagnose [-o|--output-directory <path>] [-s|--suffix <format>] [--mode=<mode>]"),
NULL
};
int cmd_diagnose(int argc, const char **argv, const char *prefix)
{
struct strbuf zip_path = STRBUF_INIT;
time_t now = time(NULL);
struct tm tm;
enum diagnose_mode mode = DIAGNOSE_STATS;
char *option_output = NULL;
char *option_suffix = "%Y-%m-%d-%H%M";
char *prefixed_filename;
const struct option diagnose_options[] = {
OPT_STRING('o', "output-directory", &option_output, N_("path"),
N_("specify a destination for the diagnostics archive")),
OPT_STRING('s', "suffix", &option_suffix, N_("format"),
N_("specify a strftime format suffix for the filename")),
OPT_CALLBACK_F(0, "mode", &mode, N_("(stats|all)"),
N_("specify the content of the diagnostic archive"),
PARSE_OPT_NONEG, option_parse_diagnose),
OPT_END()
};
argc = parse_options(argc, argv, prefix, diagnose_options,
diagnose_usage, 0);
/* Prepare the path to put the result */
prefixed_filename = prefix_filename(prefix,
option_output ? option_output : "");
strbuf_addstr(&zip_path, prefixed_filename);
strbuf_complete(&zip_path, '/');
strbuf_addstr(&zip_path, "git-diagnostics-");
strbuf_addftime(&zip_path, option_suffix, localtime_r(&now, &tm), 0, 0);
strbuf_addstr(&zip_path, ".zip");
switch (safe_create_leading_directories(zip_path.buf)) {
case SCLD_OK:
case SCLD_EXISTS:
break;
default:
die_errno(_("could not create leading directories for '%s'"),
zip_path.buf);
}
/* Prepare diagnostics */
if (create_diagnostics_archive(&zip_path, mode))
die_errno(_("unable to create diagnostics archive %s"),
zip_path.buf);
free(prefixed_filename);
strbuf_release(&zip_path);
return 0;
}

56
compat/disk.h Normal file
View File

@ -0,0 +1,56 @@
#ifndef COMPAT_DISK_H
#define COMPAT_DISK_H
#include "git-compat-util.h"
static int get_disk_info(struct strbuf *out)
{
struct strbuf buf = STRBUF_INIT;
int res = 0;
#ifdef GIT_WINDOWS_NATIVE
char volume_name[MAX_PATH], fs_name[MAX_PATH];
DWORD serial_number, component_length, flags;
ULARGE_INTEGER avail2caller, total, avail;
strbuf_realpath(&buf, ".", 1);
if (!GetDiskFreeSpaceExA(buf.buf, &avail2caller, &total, &avail)) {
error(_("could not determine free disk size for '%s'"),
buf.buf);
res = -1;
goto cleanup;
}
strbuf_setlen(&buf, offset_1st_component(buf.buf));
if (!GetVolumeInformationA(buf.buf, volume_name, sizeof(volume_name),
&serial_number, &component_length, &flags,
fs_name, sizeof(fs_name))) {
error(_("could not get info for '%s'"), buf.buf);
res = -1;
goto cleanup;
}
strbuf_addf(out, "Available space on '%s': ", buf.buf);
strbuf_humanise_bytes(out, avail2caller.QuadPart);
strbuf_addch(out, '\n');
#else
struct statvfs stat;
strbuf_realpath(&buf, ".", 1);
if (statvfs(buf.buf, &stat) < 0) {
error_errno(_("could not determine free disk size for '%s'"),
buf.buf);
res = -1;
goto cleanup;
}
strbuf_addf(out, "Available space on '%s': ", buf.buf);
strbuf_humanise_bytes(out, (off_t)stat.f_bsize * (off_t)stat.f_bavail);
strbuf_addf(out, " (mount flags 0x%lx)\n", stat.f_flag);
#endif
cleanup:
strbuf_release(&buf);
return res;
}
#endif /* COMPAT_DISK_H */

View File

@ -11,8 +11,6 @@
#include "dir.h"
#include "packfile.h"
#include "help.h"
#include "archive.h"
#include "object-store.h"
/*
* Remove the deepest subdirectory in the provided path string. Path must not
@ -262,99 +260,6 @@ static int unregister_dir(void)
return res;
}
static int add_directory_to_archiver(struct strvec *archiver_args,
const char *path, int recurse)
{
int at_root = !*path;
DIR *dir = opendir(at_root ? "." : path);
struct dirent *e;
struct strbuf buf = STRBUF_INIT;
size_t len;
int res = 0;
if (!dir)
return error_errno(_("could not open directory '%s'"), path);
if (!at_root)
strbuf_addf(&buf, "%s/", path);
len = buf.len;
strvec_pushf(archiver_args, "--prefix=%s", buf.buf);
while (!res && (e = readdir(dir))) {
if (!strcmp(".", e->d_name) || !strcmp("..", e->d_name))
continue;
strbuf_setlen(&buf, len);
strbuf_addstr(&buf, e->d_name);
if (e->d_type == DT_REG)
strvec_pushf(archiver_args, "--add-file=%s", buf.buf);
else if (e->d_type != DT_DIR)
warning(_("skipping '%s', which is neither file nor "
"directory"), buf.buf);
else if (recurse &&
add_directory_to_archiver(archiver_args,
buf.buf, recurse) < 0)
res = -1;
}
closedir(dir);
strbuf_release(&buf);
return res;
}
#ifndef WIN32
#include <sys/statvfs.h>
#endif
static int get_disk_info(struct strbuf *out)
{
#ifdef WIN32
struct strbuf buf = STRBUF_INIT;
char volume_name[MAX_PATH], fs_name[MAX_PATH];
DWORD serial_number, component_length, flags;
ULARGE_INTEGER avail2caller, total, avail;
strbuf_realpath(&buf, ".", 1);
if (!GetDiskFreeSpaceExA(buf.buf, &avail2caller, &total, &avail)) {
error(_("could not determine free disk size for '%s'"),
buf.buf);
strbuf_release(&buf);
return -1;
}
strbuf_setlen(&buf, offset_1st_component(buf.buf));
if (!GetVolumeInformationA(buf.buf, volume_name, sizeof(volume_name),
&serial_number, &component_length, &flags,
fs_name, sizeof(fs_name))) {
error(_("could not get info for '%s'"), buf.buf);
strbuf_release(&buf);
return -1;
}
strbuf_addf(out, "Available space on '%s': ", buf.buf);
strbuf_humanise_bytes(out, avail2caller.QuadPart);
strbuf_addch(out, '\n');
strbuf_release(&buf);
#else
struct strbuf buf = STRBUF_INIT;
struct statvfs stat;
strbuf_realpath(&buf, ".", 1);
if (statvfs(buf.buf, &stat) < 0) {
error_errno(_("could not determine free disk size for '%s'"),
buf.buf);
strbuf_release(&buf);
return -1;
}
strbuf_addf(out, "Available space on '%s': ", buf.buf);
strbuf_humanise_bytes(out, st_mult(stat.f_bsize, stat.f_bavail));
strbuf_addf(out, " (mount flags 0x%lx)\n", stat.f_flag);
strbuf_release(&buf);
#endif
return 0;
}
/* printf-style interface, expects `<key>=<value>` argument */
static int set_config(const char *fmt, ...)
{
@ -595,83 +500,6 @@ cleanup:
return res;
}
static void dir_file_stats_objects(const char *full_path, size_t full_path_len,
const char *file_name, void *data)
{
struct strbuf *buf = data;
struct stat st;
if (!stat(full_path, &st))
strbuf_addf(buf, "%-70s %16" PRIuMAX "\n", file_name,
(uintmax_t)st.st_size);
}
static int dir_file_stats(struct object_directory *object_dir, void *data)
{
struct strbuf *buf = data;
strbuf_addf(buf, "Contents of %s:\n", object_dir->path);
for_each_file_in_pack_dir(object_dir->path, dir_file_stats_objects,
data);
return 0;
}
static int count_files(char *path)
{
DIR *dir = opendir(path);
struct dirent *e;
int count = 0;
if (!dir)
return 0;
while ((e = readdir(dir)) != NULL)
if (!is_dot_or_dotdot(e->d_name) && e->d_type == DT_REG)
count++;
closedir(dir);
return count;
}
static void loose_objs_stats(struct strbuf *buf, const char *path)
{
DIR *dir = opendir(path);
struct dirent *e;
int count;
int total = 0;
unsigned char c;
struct strbuf count_path = STRBUF_INIT;
size_t base_path_len;
if (!dir)
return;
strbuf_addstr(buf, "Object directory stats for ");
strbuf_add_absolute_path(buf, path);
strbuf_addstr(buf, ":\n");
strbuf_add_absolute_path(&count_path, path);
strbuf_addch(&count_path, '/');
base_path_len = count_path.len;
while ((e = readdir(dir)) != NULL)
if (!is_dot_or_dotdot(e->d_name) &&
e->d_type == DT_DIR && strlen(e->d_name) == 2 &&
!hex_to_bytes(&c, e->d_name, 1)) {
strbuf_setlen(&count_path, base_path_len);
strbuf_addstr(&count_path, e->d_name);
total += (count = count_files(count_path.buf));
strbuf_addf(buf, "%s : %7d files\n", e->d_name, count);
}
strbuf_addf(buf, "Total: %d loose objects", total);
strbuf_release(&count_path);
closedir(dir);
}
static int cmd_diagnose(int argc, const char **argv)
{
struct option options[] = {
@ -681,106 +509,19 @@ static int cmd_diagnose(int argc, const char **argv)
N_("scalar diagnose [<enlistment>]"),
NULL
};
struct strbuf zip_path = STRBUF_INIT;
struct strvec archiver_args = STRVEC_INIT;
char **argv_copy = NULL;
int stdout_fd = -1, archiver_fd = -1;
time_t now = time(NULL);
struct tm tm;
struct strbuf buf = STRBUF_INIT;
struct strbuf diagnostics_root = STRBUF_INIT;
int res = 0;
argc = parse_options(argc, argv, NULL, options,
usage, 0);
setup_enlistment_directory(argc, argv, usage, options, &zip_path);
setup_enlistment_directory(argc, argv, usage, options, &diagnostics_root);
strbuf_addstr(&diagnostics_root, "/.scalarDiagnostics");
strbuf_addstr(&zip_path, "/.scalarDiagnostics/scalar_");
strbuf_addftime(&zip_path,
"%Y%m%d_%H%M%S", localtime_r(&now, &tm), 0, 0);
strbuf_addstr(&zip_path, ".zip");
switch (safe_create_leading_directories(zip_path.buf)) {
case SCLD_EXISTS:
case SCLD_OK:
break;
default:
error_errno(_("could not create directory for '%s'"),
zip_path.buf);
goto diagnose_cleanup;
}
stdout_fd = dup(1);
if (stdout_fd < 0) {
res = error_errno(_("could not duplicate stdout"));
goto diagnose_cleanup;
}
archiver_fd = xopen(zip_path.buf, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (archiver_fd < 0 || dup2(archiver_fd, 1) < 0) {
res = error_errno(_("could not redirect output"));
goto diagnose_cleanup;
}
init_zip_archiver();
strvec_pushl(&archiver_args, "scalar-diagnose", "--format=zip", NULL);
strbuf_reset(&buf);
strbuf_addstr(&buf, "Collecting diagnostic info\n\n");
get_version_info(&buf, 1);
strbuf_addf(&buf, "Enlistment root: %s\n", the_repository->worktree);
get_disk_info(&buf);
write_or_die(stdout_fd, buf.buf, buf.len);
strvec_pushf(&archiver_args,
"--add-virtual-file=diagnostics.log:%.*s",
(int)buf.len, buf.buf);
strbuf_reset(&buf);
strbuf_addstr(&buf, "--add-virtual-file=packs-local.txt:");
dir_file_stats(the_repository->objects->odb, &buf);
foreach_alt_odb(dir_file_stats, &buf);
strvec_push(&archiver_args, buf.buf);
strbuf_reset(&buf);
strbuf_addstr(&buf, "--add-virtual-file=objects-local.txt:");
loose_objs_stats(&buf, ".git/objects");
strvec_push(&archiver_args, buf.buf);
if ((res = add_directory_to_archiver(&archiver_args, ".git", 0)) ||
(res = add_directory_to_archiver(&archiver_args, ".git/hooks", 0)) ||
(res = add_directory_to_archiver(&archiver_args, ".git/info", 0)) ||
(res = add_directory_to_archiver(&archiver_args, ".git/logs", 1)) ||
(res = add_directory_to_archiver(&archiver_args, ".git/objects/info", 0)))
goto diagnose_cleanup;
strvec_pushl(&archiver_args, "--prefix=",
oid_to_hex(the_hash_algo->empty_tree), "--", NULL);
/* `write_archive()` modifies the `argv` passed to it. Let it. */
argv_copy = xmemdupz(archiver_args.v,
sizeof(char *) * archiver_args.nr);
res = write_archive(archiver_args.nr, (const char **)argv_copy, NULL,
the_repository, NULL, 0);
if (res) {
error(_("failed to write archive"));
goto diagnose_cleanup;
}
if (!res)
fprintf(stderr, "\n"
"Diagnostics complete.\n"
"All of the gathered info is captured in '%s'\n",
zip_path.buf);
diagnose_cleanup:
if (archiver_fd >= 0) {
close(1);
dup2(stdout_fd, 1);
}
free(argv_copy);
strvec_clear(&archiver_args);
strbuf_release(&zip_path);
strbuf_release(&buf);
res = run_git("diagnose", "--mode=all", "-s", "%Y%m%d_%H%M%S",
"-o", diagnostics_root.buf, NULL);
strbuf_release(&diagnostics_root);
return res;
}

View File

@ -109,14 +109,14 @@ test_expect_success UNZIP 'scalar diagnose' '
sed -n "s/.*$SQ\\(.*\\.zip\\)$SQ.*/\\1/p" <err >zip_path &&
zip_path=$(cat zip_path) &&
test -n "$zip_path" &&
unzip -v "$zip_path" &&
"$GIT_UNZIP" -v "$zip_path" &&
folder=${zip_path%.zip} &&
test_path_is_missing "$folder" &&
unzip -p "$zip_path" diagnostics.log >out &&
"$GIT_UNZIP" -p "$zip_path" diagnostics.log >out &&
test_file_not_empty out &&
unzip -p "$zip_path" packs-local.txt >out &&
"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
grep "$(pwd)/.git/objects" out &&
unzip -p "$zip_path" objects-local.txt >out &&
"$GIT_UNZIP" -p "$zip_path" objects-local.txt >out &&
grep "^Total: [1-9]" out
'

269
diagnose.c Normal file
View File

@ -0,0 +1,269 @@
#include "cache.h"
#include "diagnose.h"
#include "compat/disk.h"
#include "archive.h"
#include "dir.h"
#include "help.h"
#include "strvec.h"
#include "object-store.h"
#include "packfile.h"
struct archive_dir {
const char *path;
int recursive;
};
struct diagnose_option {
enum diagnose_mode mode;
const char *option_name;
};
static struct diagnose_option diagnose_options[] = {
{ DIAGNOSE_STATS, "stats" },
{ DIAGNOSE_ALL, "all" },
};
int option_parse_diagnose(const struct option *opt, const char *arg, int unset)
{
int i;
enum diagnose_mode *diagnose = opt->value;
if (!arg) {
*diagnose = unset ? DIAGNOSE_NONE : DIAGNOSE_STATS;
return 0;
}
for (i = 0; i < ARRAY_SIZE(diagnose_options); i++) {
if (!strcmp(arg, diagnose_options[i].option_name)) {
*diagnose = diagnose_options[i].mode;
return 0;
}
}
return error(_("invalid --%s value '%s'"), opt->long_name, arg);
}
static void dir_file_stats_objects(const char *full_path, size_t full_path_len,
const char *file_name, void *data)
{
struct strbuf *buf = data;
struct stat st;
if (!stat(full_path, &st))
strbuf_addf(buf, "%-70s %16" PRIuMAX "\n", file_name,
(uintmax_t)st.st_size);
}
static int dir_file_stats(struct object_directory *object_dir, void *data)
{
struct strbuf *buf = data;
strbuf_addf(buf, "Contents of %s:\n", object_dir->path);
for_each_file_in_pack_dir(object_dir->path, dir_file_stats_objects,
data);
return 0;
}
static int count_files(char *path)
{
DIR *dir = opendir(path);
struct dirent *e;
int count = 0;
if (!dir)
return 0;
while ((e = readdir(dir)) != NULL)
if (!is_dot_or_dotdot(e->d_name) && e->d_type == DT_REG)
count++;
closedir(dir);
return count;
}
static void loose_objs_stats(struct strbuf *buf, const char *path)
{
DIR *dir = opendir(path);
struct dirent *e;
int count;
int total = 0;
unsigned char c;
struct strbuf count_path = STRBUF_INIT;
size_t base_path_len;
if (!dir)
return;
strbuf_addstr(buf, "Object directory stats for ");
strbuf_add_absolute_path(buf, path);
strbuf_addstr(buf, ":\n");
strbuf_add_absolute_path(&count_path, path);
strbuf_addch(&count_path, '/');
base_path_len = count_path.len;
while ((e = readdir(dir)) != NULL)
if (!is_dot_or_dotdot(e->d_name) &&
e->d_type == DT_DIR && strlen(e->d_name) == 2 &&
!hex_to_bytes(&c, e->d_name, 1)) {
strbuf_setlen(&count_path, base_path_len);
strbuf_addstr(&count_path, e->d_name);
total += (count = count_files(count_path.buf));
strbuf_addf(buf, "%s : %7d files\n", e->d_name, count);
}
strbuf_addf(buf, "Total: %d loose objects", total);
strbuf_release(&count_path);
closedir(dir);
}
static int add_directory_to_archiver(struct strvec *archiver_args,
const char *path, int recurse)
{
int at_root = !*path;
DIR *dir;
struct dirent *e;
struct strbuf buf = STRBUF_INIT;
size_t len;
int res = 0;
dir = opendir(at_root ? "." : path);
if (!dir) {
if (errno == ENOENT) {
warning(_("could not archive missing directory '%s'"), path);
return 0;
}
return error_errno(_("could not open directory '%s'"), path);
}
if (!at_root)
strbuf_addf(&buf, "%s/", path);
len = buf.len;
strvec_pushf(archiver_args, "--prefix=%s", buf.buf);
while (!res && (e = readdir(dir))) {
if (!strcmp(".", e->d_name) || !strcmp("..", e->d_name))
continue;
strbuf_setlen(&buf, len);
strbuf_addstr(&buf, e->d_name);
if (e->d_type == DT_REG)
strvec_pushf(archiver_args, "--add-file=%s", buf.buf);
else if (e->d_type != DT_DIR)
warning(_("skipping '%s', which is neither file nor "
"directory"), buf.buf);
else if (recurse &&
add_directory_to_archiver(archiver_args,
buf.buf, recurse) < 0)
res = -1;
}
closedir(dir);
strbuf_release(&buf);
return res;
}
int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode)
{
struct strvec archiver_args = STRVEC_INIT;
char **argv_copy = NULL;
int stdout_fd = -1, archiver_fd = -1;
struct strbuf buf = STRBUF_INIT;
int res, i;
struct archive_dir archive_dirs[] = {
{ ".git", 0 },
{ ".git/hooks", 0 },
{ ".git/info", 0 },
{ ".git/logs", 1 },
{ ".git/objects/info", 0 }
};
if (mode == DIAGNOSE_NONE) {
res = 0;
goto diagnose_cleanup;
}
stdout_fd = dup(STDOUT_FILENO);
if (stdout_fd < 0) {
res = error_errno(_("could not duplicate stdout"));
goto diagnose_cleanup;
}
archiver_fd = xopen(zip_path->buf, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (dup2(archiver_fd, STDOUT_FILENO) < 0) {
res = error_errno(_("could not redirect output"));
goto diagnose_cleanup;
}
init_zip_archiver();
strvec_pushl(&archiver_args, "git-diagnose", "--format=zip", NULL);
strbuf_reset(&buf);
strbuf_addstr(&buf, "Collecting diagnostic info\n\n");
get_version_info(&buf, 1);
strbuf_addf(&buf, "Repository root: %s\n", the_repository->worktree);
get_disk_info(&buf);
write_or_die(stdout_fd, buf.buf, buf.len);
strvec_pushf(&archiver_args,
"--add-virtual-file=diagnostics.log:%.*s",
(int)buf.len, buf.buf);
strbuf_reset(&buf);
strbuf_addstr(&buf, "--add-virtual-file=packs-local.txt:");
dir_file_stats(the_repository->objects->odb, &buf);
foreach_alt_odb(dir_file_stats, &buf);
strvec_push(&archiver_args, buf.buf);
strbuf_reset(&buf);
strbuf_addstr(&buf, "--add-virtual-file=objects-local.txt:");
loose_objs_stats(&buf, ".git/objects");
strvec_push(&archiver_args, buf.buf);
/* Only include this if explicitly requested */
if (mode == DIAGNOSE_ALL) {
for (i = 0; i < ARRAY_SIZE(archive_dirs); i++) {
if (add_directory_to_archiver(&archiver_args,
archive_dirs[i].path,
archive_dirs[i].recursive)) {
res = error_errno(_("could not add directory '%s' to archiver"),
archive_dirs[i].path);
goto diagnose_cleanup;
}
}
}
strvec_pushl(&archiver_args, "--prefix=",
oid_to_hex(the_hash_algo->empty_tree), "--", NULL);
/* `write_archive()` modifies the `argv` passed to it. Let it. */
argv_copy = xmemdupz(archiver_args.v,
sizeof(char *) * archiver_args.nr);
res = write_archive(archiver_args.nr, (const char **)argv_copy, NULL,
the_repository, NULL, 0);
if (res) {
error(_("failed to write archive"));
goto diagnose_cleanup;
}
fprintf(stderr, "\n"
"Diagnostics complete.\n"
"All of the gathered info is captured in '%s'\n",
zip_path->buf);
diagnose_cleanup:
if (archiver_fd >= 0) {
dup2(stdout_fd, STDOUT_FILENO);
close(stdout_fd);
close(archiver_fd);
}
free(argv_copy);
strvec_clear(&archiver_args);
strbuf_release(&buf);
return res;
}

17
diagnose.h Normal file
View File

@ -0,0 +1,17 @@
#ifndef DIAGNOSE_H
#define DIAGNOSE_H
#include "strbuf.h"
#include "parse-options.h"
enum diagnose_mode {
DIAGNOSE_NONE,
DIAGNOSE_STATS,
DIAGNOSE_ALL
};
int option_parse_diagnose(const struct option *opt, const char *arg, int unset);
int create_diagnostics_archive(struct strbuf *zip_path, enum diagnose_mode mode);
#endif /* DIAGNOSE_H */

View File

@ -261,6 +261,7 @@ static inline int is_xplatform_dir_sep(int c)
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/statvfs.h>
#include <termios.h>
#ifndef NO_SYS_SELECT_H
#include <sys/select.h>

1
git.c
View File

@ -522,6 +522,7 @@ static struct cmd_struct commands[] = {
{ "credential-cache--daemon", cmd_credential_cache_daemon },
{ "credential-store", cmd_credential_store },
{ "describe", cmd_describe, RUN_SETUP },
{ "diagnose", cmd_diagnose, RUN_SETUP_GENTLY },
{ "diff", cmd_diff, NO_PARSEOPT },
{ "diff-files", cmd_diff_files, RUN_SETUP | NEED_WORK_TREE | NO_PARSEOPT },
{ "diff-index", cmd_diff_index, RUN_SETUP | NO_PARSEOPT },

View File

@ -78,4 +78,52 @@ test_expect_success 'indicates populated hooks' '
test_cmp expect actual
'
test_expect_success UNZIP '--diagnose creates diagnostics zip archive' '
test_when_finished rm -rf report &&
git bugreport --diagnose -o report -s test >out &&
zip_path=report/git-diagnostics-test.zip &&
grep "Available space" out &&
test_path_is_file "$zip_path" &&
# Check zipped archive content
"$GIT_UNZIP" -p "$zip_path" diagnostics.log >out &&
test_file_not_empty out &&
"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
grep ".git/objects" out &&
"$GIT_UNZIP" -p "$zip_path" objects-local.txt >out &&
grep "^Total: [0-9][0-9]*" out &&
# Should not include .git directory contents by default
! "$GIT_UNZIP" -l "$zip_path" | grep ".git/"
'
test_expect_success UNZIP '--diagnose=stats excludes .git dir contents' '
test_when_finished rm -rf report &&
git bugreport --diagnose=stats -o report -s test >out &&
# Includes pack quantity/size info
"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
grep ".git/objects" out &&
# Does not include .git directory contents
! "$GIT_UNZIP" -l "$zip_path" | grep ".git/"
'
test_expect_success UNZIP '--diagnose=all includes .git dir contents' '
test_when_finished rm -rf report &&
git bugreport --diagnose=all -o report -s test >out &&
# Includes .git directory contents
"$GIT_UNZIP" -l "$zip_path" | grep ".git/" &&
"$GIT_UNZIP" -p "$zip_path" .git/HEAD >out &&
test_file_not_empty out
'
test_done

60
t/t0092-diagnose.sh Executable file
View File

@ -0,0 +1,60 @@
#!/bin/sh
test_description='git diagnose'
TEST_PASSES_SANITIZE_LEAK=true
. ./test-lib.sh
test_expect_success UNZIP 'creates diagnostics zip archive' '
test_when_finished rm -rf report &&
git diagnose -o report -s test >out &&
grep "Available space" out &&
zip_path=report/git-diagnostics-test.zip &&
test_path_is_file "$zip_path" &&
# Check zipped archive content
"$GIT_UNZIP" -p "$zip_path" diagnostics.log >out &&
test_file_not_empty out &&
"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
grep ".git/objects" out &&
"$GIT_UNZIP" -p "$zip_path" objects-local.txt >out &&
grep "^Total: [0-9][0-9]*" out &&
# Should not include .git directory contents by default
! "$GIT_UNZIP" -l "$zip_path" | grep ".git/"
'
test_expect_success UNZIP '--mode=stats excludes .git dir contents' '
test_when_finished rm -rf report &&
git diagnose -o report -s test --mode=stats >out &&
# Includes pack quantity/size info
"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
grep ".git/objects" out &&
# Does not include .git directory contents
! "$GIT_UNZIP" -l "$zip_path" | grep ".git/"
'
test_expect_success UNZIP '--mode=all includes .git dir contents' '
test_when_finished rm -rf report &&
git diagnose -o report -s test --mode=all >out &&
# Includes pack quantity/size info
"$GIT_UNZIP" -p "$zip_path" packs-local.txt >out &&
grep ".git/objects" out &&
# Includes .git directory contents
"$GIT_UNZIP" -l "$zip_path" | grep ".git/" &&
"$GIT_UNZIP" -p "$zip_path" .git/HEAD >out &&
test_file_not_empty out
'
test_done