diff --git a/Documentation/config/transfer.txt b/Documentation/config/transfer.txt index f1ce50f4a6e6ba..28a08f21fc33d6 100644 --- a/Documentation/config/transfer.txt +++ b/Documentation/config/transfer.txt @@ -125,3 +125,19 @@ transfer.bundleURI:: transfer.advertiseObjectInfo:: When `true`, the `object-info` capability is advertised by servers. Defaults to false. + +transfer.advertiseOSVersion:: + When `true`, the `os-version` capability is advertised by clients and + servers. It makes clients and servers send to each other a string + representing the operating system name, like "Linux" or "Windows". + This string is retrieved from the 'sysname' field of the struct returned + by the uname(2) system call. If the `osVersion.command` is set, the + output of the command specified will be the string exchanged by the clients + and the servers. Defaults to true. + +osVersion.command:: + If this variable is set, the specified command will be run and the output + will be used as the value `X` for `os-version` capability (in the form + `os-version=X`). `osVersion.command` is only used if `transfer.advertiseOSVersion` + is true. Refer to the linkgit:git-config[1] documentation to learn more about + `transfer.advertiseOSVersion` config option. diff --git a/Documentation/gitprotocol-v2.txt b/Documentation/gitprotocol-v2.txt index 1652fef3aeb166..53621c0bce81c8 100644 --- a/Documentation/gitprotocol-v2.txt +++ b/Documentation/gitprotocol-v2.txt @@ -190,6 +190,27 @@ printable ASCII characters except space (i.e., the byte range 32 < x < and debugging purposes, and MUST NOT be used to programmatically assume the presence or absence of particular features. +os-version +~~~~~~~~~~ + +In the same way as the `agent` capability above, the server can +advertise the `os-version` capability with a value `X` (in the form +`os-version=X`) to notify the client that the server is running an +operating system that can be identified by `X`. The client may +optionally send its own `os-version` string by including the +`os-version` capability with a value `Y` (in the form `os-version=Y`) +in its request to the server (but it MUST NOT do so if the server did +not advertise the os-version capability). The `X` and `Y` strings may +contain any printable ASCII characters except space (i.e., the byte +range 32 < x < 127), and are typically made from the result of +`uname -s`(OS name e.g Linux). If the `osVersion.command` is set, +the `X` and `Y` are made from the ouput of the command specified. +The os-version capability can be disabled entirely by setting the +`transfer.advertiseOSVersion` config option to `false`. The `os-version` +strings are purely informative for statistics and debugging purposes, and +MUST NOT be used to programmatically assume the presence or absence of +particular features. + ls-refs ~~~~~~~ diff --git a/builtin/bugreport.c b/builtin/bugreport.c index 0ac59cc8dc5824..66d64bfd5aec25 100644 --- a/builtin/bugreport.c +++ b/builtin/bugreport.c @@ -12,10 +12,10 @@ #include "diagnose.h" #include "object-file.h" #include "setup.h" +#include "version.h" static void get_system_info(struct strbuf *sys_info) { - struct utsname uname_info; char *shell = NULL; /* get git version from native cmd */ @@ -24,16 +24,7 @@ static void get_system_info(struct strbuf *sys_info) /* system call for other version info */ strbuf_addstr(sys_info, "uname: "); - if (uname(&uname_info)) - strbuf_addf(sys_info, _("uname() failed with error '%s' (%d)\n"), - strerror(errno), - errno); - else - strbuf_addf(sys_info, "%s %s %s %s\n", - uname_info.sysname, - uname_info.release, - uname_info.version, - uname_info.machine); + get_uname_info(sys_info, 1); strbuf_addstr(sys_info, _("compiler info: ")); get_compiler_info(sys_info); diff --git a/connect.c b/connect.c index 2b51cf09bf18d0..51728da6e91fe1 100644 --- a/connect.c +++ b/connect.c @@ -492,6 +492,9 @@ static void send_capabilities(int fd_out, struct packet_reader *reader) if (server_supports_v2("agent")) packet_write_fmt(fd_out, "agent=%s", git_user_agent_sanitized()); + if (server_supports_v2("os-version") && advertise_os_version(the_repository)) + packet_write_fmt(fd_out, "os-version=%s", os_version_sanitized()); + if (server_feature_v2("object-format", &hash_name)) { int hash_algo = hash_algo_by_name(hash_name); if (hash_algo == GIT_HASH_UNKNOWN) diff --git a/serve.c b/serve.c index 92fd26fd0a8bdd..a73735e757ea72 100644 --- a/serve.c +++ b/serve.c @@ -29,6 +29,16 @@ static int agent_advertise(struct repository *r UNUSED, return 1; } +static int os_version_advertise(struct repository *r, + struct strbuf *value) +{ + if (!advertise_os_version(r)) + return 0; + if (value) + strbuf_addstr(value, os_version_sanitized()); + return 1; +} + static int object_format_advertise(struct repository *r, struct strbuf *value) { @@ -123,6 +133,10 @@ static struct protocol_capability capabilities[] = { .name = "agent", .advertise = agent_advertise, }, + { + .name = "os-version", + .advertise = os_version_advertise, + }, { .name = "ls-refs", .advertise = ls_refs_advertise, diff --git a/t/t5555-http-smart-common.sh b/t/t5555-http-smart-common.sh index e47ea1ad106048..8d5844eaf20e65 100755 --- a/t/t5555-http-smart-common.sh +++ b/t/t5555-http-smart-common.sh @@ -123,9 +123,48 @@ test_expect_success 'git receive-pack --advertise-refs: v1' ' ' test_expect_success 'git upload-pack --advertise-refs: v2' ' + printf "agent=FAKE" >agent_and_os_name && + if test_have_prereq WINDOWS + then + # We do not use test_config here so that any tests below can reuse + # the "expect" file from this test + git config transfer.advertiseOSVersion false + else + printf "\nos-version=%s\n" $(uname -s | test_redact_non_printables) >>agent_and_os_name + fi && + + cat >expect <<-EOF && + version 2 + $(cat agent_and_os_name) + ls-refs=unborn + fetch=shallow wait-for-done + server-option + object-format=$(test_oid algo) + 0000 + EOF + + GIT_PROTOCOL=version=2 \ + GIT_USER_AGENT=FAKE \ + git upload-pack --advertise-refs . >out 2>err && + + test-tool pkt-line unpack actual && + test_must_be_empty err && + test_cmp actual expect +' + +test_expect_success 'git upload-pack --advertise-refs: v2 with osVersion.command config set' ' + # test_config is used here as we are not reusing any file output from here + test_config osVersion.command "uname -srvm" && + printf "agent=FAKE" >agent_and_long_os_name && + + if test_have_prereq !WINDOWS + then + printf "\nos-version=%s\n" $(uname -srvm | test_redact_non_printables) >>agent_and_long_os_name + fi && + cat >expect <<-EOF && version 2 - agent=FAKE + $(cat agent_and_long_os_name) ls-refs=unborn fetch=shallow wait-for-done server-option diff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh index de904c165501c9..51d99cd62c6c6b 100755 --- a/t/t5701-git-serve.sh +++ b/t/t5701-git-serve.sh @@ -8,13 +8,23 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME . ./test-lib.sh test_expect_success 'test capability advertisement' ' + printf "agent=git/$(git version | cut -d" " -f3)" >agent_and_os_name && + if test_have_prereq WINDOWS + then + # We do not use test_config here so that tests below will be able to reuse + # the expect.base and expect.trailer files + git config transfer.advertiseOSVersion false + else + printf "\nos-version=%s\n" $(uname -s | test_redact_non_printables) >>agent_and_os_name + fi && + test_oid_cache <<-EOF && wrong_algo sha1:sha256 wrong_algo sha256:sha1 EOF cat >expect.base <<-EOF && version 2 - agent=git/$(git version | cut -d" " -f3) + $(cat agent_and_os_name) ls-refs=unborn fetch=shallow wait-for-done server-option @@ -31,6 +41,39 @@ test_expect_success 'test capability advertisement' ' test_cmp expect actual ' +test_expect_success 'test capability advertisement with osVersion.command config set' ' + # test_config is used here as we are not reusing any file output from here + test_config osVersion.command "uname -srvm" && + printf "agent=git/$(git version | cut -d" " -f3)" >agent_and_long_os_name && + + if test_have_prereq !WINDOWS + then + printf "\nos-version=%s\n" $(uname -srvm | test_redact_non_printables) >>agent_and_long_os_name + fi && + + test_oid_cache <<-EOF && + wrong_algo sha1:sha256 + wrong_algo sha256:sha1 + EOF + cat >expect.base_long <<-EOF && + version 2 + $(cat agent_and_long_os_name) + ls-refs=unborn + fetch=shallow wait-for-done + server-option + object-format=$(test_oid algo) + EOF + cat >expect.trailer_long <<-EOF && + 0000 + EOF + cat expect.base_long expect.trailer_long >expect && + + GIT_TEST_SIDEBAND_ALL=0 test-tool serve-v2 \ + --advertise-capabilities >out && + test-tool pkt-line unpack actual && + test_cmp expect actual +' + test_expect_success 'stateless-rpc flag does not list capabilities' ' # Empty request test-tool pkt-line pack >in <<-EOF && diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 74b9a82129bedc..b6241adcb44ec2 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -2043,3 +2043,11 @@ test_trailing_hash () { test-tool hexdump | sed "s/ //g" } + +# Trim and replace each character with ascii code below 32 or above +# 127 (included) using a dot '.' character. +# Octal intervals \001-\040 and \177-\377 +# corresponds to decimal intervals 1-32 and 127-255 +test_redact_non_printables () { + tr -d "\n" | tr "[\001-\040][\177-\377]" "." +} diff --git a/version.c b/version.c index 4d763ab48dd76c..b4462328987772 100644 --- a/version.c +++ b/version.c @@ -1,11 +1,31 @@ +#define USE_THE_REPOSITORY_VARIABLE + #include "git-compat-util.h" #include "version.h" #include "version-def.h" #include "strbuf.h" +#include "gettext.h" +#include "config.h" +#include "run-command.h" +#include "alias.h" const char git_version_string[] = GIT_VERSION; const char git_built_from_commit_string[] = GIT_BUILT_FROM_COMMIT; +/* + * Trim and replace each character with ascii code below 32 or above + * 127 (included) using a dot '.' character. + * TODO: ensure consecutive non-printable characters are only replaced once +*/ +static void redact_non_printables(struct strbuf *buf) +{ + strbuf_trim(buf); + for (size_t i = 0; i < buf->len; i++) { + if (buf->buf[i] <= 32 || buf->buf[i] >= 127) + buf->buf[i] = '.'; + } +} + const char *git_user_agent(void) { static const char *agent = NULL; @@ -27,13 +47,117 @@ const char *git_user_agent_sanitized(void) struct strbuf buf = STRBUF_INIT; strbuf_addstr(&buf, git_user_agent()); - strbuf_trim(&buf); - for (size_t i = 0; i < buf.len; i++) { - if (buf.buf[i] <= 32 || buf.buf[i] >= 127) - buf.buf[i] = '.'; - } - agent = buf.buf; + redact_non_printables(&buf); + agent = strbuf_detach(&buf, NULL); } return agent; } + +int get_uname_info(struct strbuf *buf, unsigned int full) +{ + struct utsname uname_info; + + if (uname(&uname_info)) { + strbuf_addf(buf, _("uname() failed with error '%s' (%d)\n"), + strerror(errno), + errno); + return -1; + } + + if (full) + strbuf_addf(buf, "%s %s %s %s\n", + uname_info.sysname, + uname_info.release, + uname_info.version, + uname_info.machine); + else + strbuf_addf(buf, "%s\n", uname_info.sysname); + return 0; +} + +/* + * Return -1 if unable to retrieve the osversion.command config or + * if the command is malformed; otherwise, return 0 if successful. + */ +static int fill_os_version_command(struct child_process *cmd) +{ + const char *os_version_command; + const char **argv; + char *os_version_copy; + int n; + + if (git_config_get_string_tmp("osversion.command", &os_version_command)) + return -1; + + os_version_copy = xstrdup(os_version_command); + n = split_cmdline(os_version_copy, &argv); + + if (n < 0) { + warning(_("malformed osVersion.command config option: %s"), + _(split_cmdline_strerror(n))); + free(os_version_copy); + return -1; + } + + for (int i = 0; i < n; i++) + strvec_push(&cmd->args, argv[i]); + free(os_version_copy); + free(argv); + + return 0; +} + +static int capture_os_version(struct strbuf *buf) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + if (fill_os_version_command(&cmd)) + return -1; + if (capture_command(&cmd, buf, 0)) + return -1; + + return 0; +} + +const char *os_version(void) +{ + static const char *os = NULL; + + if (!os) { + struct strbuf buf = STRBUF_INIT; + + if (capture_os_version(&buf)) + get_uname_info(&buf, 0); + os = strbuf_detach(&buf, NULL); + } + + return os; +} + +const char *os_version_sanitized(void) +{ + static const char *os_sanitized = NULL; + + if (!os_sanitized) { + struct strbuf buf = STRBUF_INIT; + + strbuf_addstr(&buf, os_version()); + redact_non_printables(&buf); + os_sanitized = strbuf_detach(&buf, NULL); + } + + return os_sanitized; +} + +int advertise_os_version(struct repository *r) +{ + static int transfer_advertise_os_version = -1; + + if (transfer_advertise_os_version == -1) { + repo_config_get_bool(r, "transfer.advertiseosversion", &transfer_advertise_os_version); + /* enabled by default */ + transfer_advertise_os_version = !!transfer_advertise_os_version; + } + return transfer_advertise_os_version; +} diff --git a/version.h b/version.h index 7c62e80577154d..8167ce956a6dfb 100644 --- a/version.h +++ b/version.h @@ -1,10 +1,23 @@ #ifndef VERSION_H #define VERSION_H +struct repository; + extern const char git_version_string[]; extern const char git_built_from_commit_string[]; const char *git_user_agent(void); const char *git_user_agent_sanitized(void); +/* + Try to get information about the system using uname(2). + Return -1 and put an error message into 'buf' in case of uname() + error. Return 0 and put uname info into 'buf' otherwise. +*/ +int get_uname_info(struct strbuf *buf, unsigned int full); + +const char *os_version(void); +const char *os_version_sanitized(void); +int advertise_os_version(struct repository *r); + #endif /* VERSION_H */