test-chmtime: use parse_options() to parse command-line flags
PATCH 1/1 test-chmtime: use parse_options() to parse command-line flags
From: Shashank S Mogaveera <[email protected]>
cmd__chmtime() currently parses the '-v'/'--verbose' and '-g'/'--get' flags manually using strcmp() checks and manual index tracking.
Refactor cmd__chmtime() to use parse_options() and usage_with_options() instead. Pass PARSE_OPT_KEEP_UNKNOWN_OPT so that negative relative time offsets (such as '-86400') are not rejected as unknown short options and remain in argv for timespec_arg() to parse.
---
[Outreachy]test-chmtime: use parse_options() to parse command-line flags
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2280%2Fshashu8660%2Foutreachy3-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2280/shashu8660/outreachy3-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/2280
t/helper/test-chmtime.c | 46 +++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 25 deletions(-)
t/helper/test-chmtime.c +21 -25
@@ -38,10 +38,13 @@*/ #include "test-tool.h" #include "git-compat-util.h" +#include "parse-options.h" #include <utime.h> -static const char usage_str[] = -"(-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>..." ; +static const char *const chmtime_usage[] = { +"test-tool chmtime (-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>..." , +NULL +};static int timespec_arg (const char *arg,long int *set_time,int *set_eq) {
@@ -68,40 +71,37 @@ static int timespec_arg(const char *arg, long int *set_time, int *set_eq)int cmd__chmtime (int argc,const char **argv) { -static int verbose; -static int get; +int verbose =0 ; +int get =0 ; -int i =1 ; +int i =0 ;/* no mtime change by default */ int set_eq =0 ;long int set_time =0 ; -if (argc <3 ) - goto usage; +struct option options[] = { +OPT_BOOL ('v' ,"verbose" , &verbose,"print the mtime and the file name" ), +OPT_BOOL ('g' ,"get" , &get,"print only the mtime" ), +OPT_END () + }; -if (strcmp (argv[i],"--get" ) ==0 ||strcmp (argv[i],"-g" ) ==0 ) { - get =1 ; - ++i; - }else if (strcmp (argv[i],"--verbose" ) ==0 ||strcmp (argv[i],"-v" ) ==0 ) { - verbose =1 ; - ++i; - } + argc =parse_options (argc, argv,NULL , options, chmtime_usage, +PARSE_OPT_KEEP_UNKNOWN_OPT ); -if (i == argc) { - goto usage; - } +if (argc <1 || (!get && !verbose && argc <2 )) +usage_with_options (chmtime_usage, options);if (timespec_arg (argv[i], &set_time, &set_eq)) { ++i; }else {if (get ==0 && verbose ==0 ) {fprintf (stderr,"Not a base-10 integer: %s\n" , argv[i] +1 ); - goto usage; +usage_with_options (chmtime_usage, options); } }if (i == argc) - goto usage; +usage_with_options (chmtime_usage, options);for (; i < argc; i++) {struct stat sb;
@@ -110,7 +110,7 @@ int cmd__chmtime(int argc, const char **argv)if (stat (argv[i], &sb) <0 ) {fprintf (stderr,"Failed to stat %s: %s. Skipping\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));continue ; }
@@ -150,14 +150,10 @@ int cmd__chmtime(int argc, const char **argv) }#endif fprintf (stderr,"Failed to modify time on %s: %s\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));return 1 ; } }return 0 ; - -usage: -fprintf (stderr,"usage: %s %s\n" , argv[0 ], usage_str); -return 1 ; }
base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e
Signature · 1 line
gitgitgadget
Quoted 9 lines · Shashank S via GitGitGadget
From: Shashank S Mogaveera <[email protected]>
cmd__chmtime()currently parses the '-v'/'--verbose' and '-g'/'--get' flags manually usingstrcmp()checks and manual index tracking.Refactor
cmd__chmtime()to useparse_options()andusage_with_options()instead. PassPARSE_OPT_KEEP_UNKNOWN_OPTso that negative relative time offsets (such as '-86400') are not rejected as unknown short options and remain in argv fortimespec_arg()to parse.
I somehow find the verb "refactor" used to describe a change like this slightly awkward, but it is excellent to reminder readers why it is necessary to use PARSE_OPT_KEEP_UNKNOWN_OPT.
Quoted 10 lines
t/helper/test-chmtime.c | 46 +++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 25 deletions(-)t/helper/test-chmtime.c +0
-0@@ -38,10 +38,13 @@*/ #include "test-tool.h"
t/helper/test-chmtime.c line 47
Quoted 10 lines
t/helper/test-chmtime.c +5
-2#include "git-compat-util.h" +#include "parse-options.h" #include <utime.h> -static const char usage_str[] = -"(-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>..." ; +static const char *const chmtime_usage[] = { +"test-tool chmtime (-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>..." , +NULL +};
Is it necessary to spell out the options like verbose and get, once you switch to the parse-options API? What does "test-tool chmtime -h" say with this patch applied? I think it is common to say [<options>] instead and let the list of options produced by the parse-options API to take it from there.
t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 75
Quoted 10 lines
t/helper/test-chmtime.c +2
-2static int timespec_arg (const char *arg,long int *set_time,int *set_eq) {@@ -68,40 +71,37 @@ static int timespec_arg(const char *arg, long int *set_time, int *set_eq)int cmd__chmtime (int argc,const char **argv) { -static int verbose; -static int get; +int verbose =0 ; +int get =0 ;
There is no reason why these two need to be static, but as a "main()" function for a run-once-and-exit program, there is no reason why they should not be static, either. With this change, you are forced to explicitly initialize them to 0.
Quoted 24 lines
t/helper/test-chmtime.c +0
-13-int i =1 ;/* no mtime change by default */ int set_eq =0 ;long int set_time =0 ; -if (argc <3 ) - goto usage; -if (strcmp (argv[i],"--get" ) ==0 ||strcmp (argv[i],"-g" ) ==0 ) { - get =1 ; - ++i; - }else if (strcmp (argv[i],"--verbose" ) ==0 ||strcmp (argv[i],"-v" ) ==0 ) { - verbose =1 ; - ++i; - } -if (i == argc) { - goto usage; - }if (timespec_arg (argv[i], &set_time, &set_eq)) { ++i; }else {if (get ==0 && verbose ==0 ) {
t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 107
Quoted 10 lines
t/helper/test-chmtime.c +0
-2fprintf (stderr,"Not a base-10 integer: %s\n" , argv[i] +1 ); - goto usage; } }if (i == argc) - goto usage;for (; i < argc; i++) {struct stat sb;
I agree that the original was horrible in that it did not allow -v to come before -g etc.
Now, let's read the updated code.
Quoted 7 lines
t/helper/test-chmtime.c +2
-0+int i =0 ;/* no mtime change by default */ int set_eq =0 ;long int set_time =0 ; +struct option options[] = {
t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 92
Quoted 10 lines
t/helper/test-chmtime.c +8
-0+OPT_BOOL ('v' ,"verbose" , &verbose,"print the mtime and the file name" ), +OPT_BOOL ('g' ,"get" , &get,"print only the mtime" ), + OPT_END() + }; + argc =parse_options (argc, argv,NULL , options, chmtime_usage, +PARSE_OPT_KEEP_UNKNOWN_OPT ); +if (argc <1 || (!get && !verbose && argc <2 )) +usage_with_options (chmtime_usage, options);
I am not sure what is going on here. The command, when parsing out the -g and -v options, expects to have <seconds> and 1 or more <file> arguments. Why should it reject argc==2 when neither -g nor -v is given, but allow argc==2 when -g and/or -v is?
OK, the usage of this command is a bit opaque. The command works in two ways.
* When -v/-g is given, it is a request to show the timestamp of the file, so <seconds> becomes optional (i.e., when given, it sets and then shows). We still want at least one <file>.
* Otherwise, it is a request to set the timestamp, so <seconds> is required, and we want at least one <file>.
Not the best way to design an end-user UI, but this is merely a test helper, so let's not complain too much ;-).
t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 101
Quoted 8 lines
t/helper/test-chmtime.c +1
-0if (timespec_arg (argv[i], &set_time, &set_eq)) { ++i; }else {if (get ==0 && verbose ==0 ) {fprintf (stderr,"Not a base-10 integer: %s\n" , argv[i] +1 ); +usage_with_options (chmtime_usage, options); } }
This is largely the fault of the original, but I wonder if we can avoid the "without -v/-g, timespec is required" logic in two separate places like this (and the earlier one that puzzled me)?
t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 104
Quoted 2 lines
t/helper/test-chmtime.c +1
-0if (i == argc) +usage_with_options (chmtime_usage, options);
And we want at least one <file> in any case.
t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 107
Quoted 2 lines
t/helper/test-chmtime.c +0
-0for (; i < argc; i++) {struct stat sb;
OK. We will iterate over the <file> arguments given.
t/helper/test-chmtime.c int cmd__chmtime(int argc, const char **argv) · line 115
Quoted 8 lines
t/helper/test-chmtime.c +1
-1@@ -110,7 +110,7 @@ int cmd__chmtime(int argc, const char **argv)if (stat (argv[i], &sb) <0 ) {fprintf (stderr,"Failed to stat %s: %s. Skipping\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));continue ; }
This fixes a whitespace breakage, which is an unrelated change, but I'll let it pass.
t/helper/test-chmtime.c int cmd__chmtime(int argc, const char **argv) · line 156
Quoted 9 lines
t/helper/test-chmtime.c +1
-1@@ -150,14 +150,10 @@ int cmd__chmtime(int argc, const char **argv) }#endif fprintf (stderr,"Failed to modify time on %s: %s\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));return 1 ; } }
Likewise.
t/helper/test-chmtime.c int cmd__chmtime(int argc, const char **argv) · line 159
Quoted 7 lines
t/helper/test-chmtime.c +0
-4return 0 ; - -usage: -fprintf (stderr,"usage: %s %s\n" , argv[0 ], usage_str); -return 1 ; }
We exit via usage_with_options() with 129 now, but used to exit(1) upon a command line error.
Looking good. Thanks.
On Wed, 28 Oct 2026 at 00:08, Junio C Hamano <[email protected]> wrote:
Thank you for the detailed review and for pointing these out!
Quoted 6 lines
Quoted 1 line
and remain in argv for
timespec_arg()to parse.I somehow find the verb "refactor" used to describe a change like this slightly awkward, but it is excellent to reminder readers why it is necessary to use
PARSE_OPT_KEEP_UNKNOWN_OPT.
Understood! In my v2, I will change the commit message to use "update" instead of "refactor".
Quoted 14 lines
Quoted 14 lines
t/helper/test-chmtime.c | 46 +++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 25 deletions(-)t/helper/test-chmtime.c +1
-0@@ -38,10 +38,13 @@*/ #include "test-tool.h" #include "git-compat-util.h" +#include "parse-options.h" #include <utime.h>
Quoted 13 lines
t/helper/test-chmtime.c line 47
Quoted 6 lines
t/helper/test-chmtime.c +4
-2-static const char usage_str[] = -"(-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>..." ; +static const char *const chmtime_usage[] = { +"test-tool chmtime (-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>..." , +NULL +};Is it necessary to spell out the options like verbose and get, once you switch to the parse-options API? What does "test-tool chmtime -h" say with this patch applied? I think it is common to say [<options>] instead and let the list of options produced by the parse-options API to take it from there.
You are completely right. Hardcoding the flags in the string is redundant now that parse_options() handles the -h output. I will update the usage string to [<options>] in v2.
Quoted 4 lines
Quoted 4 lines
t/helper/test-chmtime.c +0
-0static int timespec_arg (const char *arg,long int *set_time,int *set_eq) {@@ -68,40 +71,37 @@ static int timespec_arg(const char *arg, long int *set_time, int *set_eq)
Quoted 12 lines
t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 75
Quoted 6 lines
t/helper/test-chmtime.c +2
-2int cmd__chmtime (int argc,const char **argv) { -static int verbose; -static int get; +int verbose =0 ; +int get =0 ;There is no reason why these two need to be static, but as a "
main()" function for a run-once-and-exit program, there is no reason why they should not be static, either. With this change, you are forced to explicitly initialize them to 0.
That is a great point about the automatic initialization of static variables. Since it isn't strictly necessary to change them for parse_options() to work, I will revert this specific change in v2 to keep the diff as small and focused as possible.
Quoted 70 lines
Quoted 24 lines
t/helper/test-chmtime.c +0
-13-int i =1 ;/* no mtime change by default */ int set_eq =0 ;long int set_time =0 ; -if (argc <3 ) - goto usage; -if (strcmp (argv[i],"--get" ) ==0 ||strcmp (argv[i],"-g" ) ==0 ) { - get =1 ; - ++i; - }else if (strcmp (argv[i],"--verbose" ) ==0 ||strcmp (argv[i],"-v" ) ==0 ) { - verbose =1 ; - ++i; - } -if (i == argc) { - goto usage; - }if (timespec_arg (argv[i], &set_time, &set_eq)) { ++i; }else {if (get ==0 && verbose ==0 ) {t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 107
Quoted 10 lines
t/helper/test-chmtime.c +0
-2fprintf (stderr,"Not a base-10 integer: %s\n" , argv[i] +1 ); - goto usage; } }if (i == argc) - goto usage;for (; i < argc; i++) {struct stat sb;I agree that the original was horrible in that it did not allow -v to come before -g etc.
Now, let's read the updated code.
Quoted 7 lines
t/helper/test-chmtime.c +2
-0+int i =0 ;/* no mtime change by default */ int set_eq =0 ;long int set_time =0 ; +struct option options[] = {t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 92
Quoted 10 lines
t/helper/test-chmtime.c +8
-0+OPT_BOOL ('v' ,"verbose" , &verbose,"print the mtime and the file name" ), +OPT_BOOL ('g' ,"get" , &get,"print only the mtime" ), + OPT_END() + }; + argc =parse_options (argc, argv,NULL , options, chmtime_usage, +PARSE_OPT_KEEP_UNKNOWN_OPT ); +if (argc <1 || (!get && !verbose && argc <2 )) +usage_with_options (chmtime_usage, options);I am not sure what is going on here. The command, when parsing out the -g and -v options, expects to have <seconds> and 1 or more <file> arguments. Why should it reject argc==2 when neither -g nor -v is given, but allow argc==2 when -g and/or -v is?
OK, the usage of this command is a bit opaque. The command works in two ways.
* When -v/-g is given, it is a request to show the timestamp of the file, so <seconds> becomes optional (i.e., when given, it sets and then shows). We still want at least one <file>.
Quoted 6 lines
* Otherwise, it is a request to set the timestamp, so <seconds> is required, and we want at least one <file>.
Not the best way to design an end-user UI, but this is merely a test helper, so let's not complain too much ;-).
I completely agree, the original logic was very opaque. By removing the initial (!get && !verbose && argc < 2) check, we can rely entirely on the timespec_arg() check to validate the <seconds> argument, and then simply use if (i == argc) at the end to ensure <file> arguments were provided. I will clean this flow up significantly in v2.
Quoted 49 lines
Quoted 8 lines
if (timespec_arg(argv[i], &set_time, &set_eq)) { ++i; } else { if (get == 0 && verbose == 0) { fprintf(stderr, "Not a base-10 integer: %s\n", argv[i] + 1); + usage_with_options(chmtime_usage, options); } }This is largely the fault of the original, but I wonder if we can avoid the "without -v/-g, timespec is required" logic in two separate places like this (and the earlier one that puzzled me)?
Quoted 2 lines
if (i == argc)+
usage_with_options(chmtime_usage, options);And we want at least one <file> in any case.
Quoted 2 lines
for (; i < argc; i++) { struct stat sb;OK. We will iterate over the <file> arguments given.
t/helper/test-chmtime.c int cmd__chmtime(int argc, const char **argv) · line 115
Quoted 8 lines
t/helper/test-chmtime.c +1
-1@@ -110,7 +110,7 @@ int cmd__chmtime(int argc, const char **argv)if (stat (argv[i], &sb) <0 ) {fprintf (stderr,"Failed to stat %s: %s. Skipping\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));continue ; }This fixes a whitespace breakage, which is an unrelated change, but I'll let it pass.
t/helper/test-chmtime.c int cmd__chmtime(int argc, const char **argv) · line 156
Quoted 9 lines
t/helper/test-chmtime.c +1
-1@@ -150,14 +150,10 @@ int cmd__chmtime(int argc, const char **argv) }#endif fprintf (stderr,"Failed to modify time on %s: %s\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));return 1 ; } }Likewise.
Quoted 1 line
Quoted 10 lines
t/helper/test-chmtime.c int cmd__chmtime(int argc, const char **argv) · line 159
Quoted 6 lines
return 0;t/helper/test-chmtime.c +0
-4- -usage: -fprintf (stderr,"usage: %s %s\n" , argv[0 ], usage_str); -return 1 ; }We exit via
usage_with_options()with 129 now, but used to exit(1) upon a command line error.
which is a nice bonus of migrating to the parse-options API.
Quoted 1 line
Looking good. Thanks.
Thanks
Shashank S Mogaveera
Quoted 3 lines · Shashank S via GitGitGadget
t/helper/test-chmtime.c | 46 +++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 25 deletions(-)
Forgot to mention one thing. Do we have any tests to cover the behaviour of this test helper? If not, should we add some (not a rhetorical question)?
Also, one thing I missed is that the original ignored --verbose when --get was given, since they contradict with each other. Shouldn't we make them explicitly incompatible with each other, perhaps before where we call timespec_arg()?
t/helper/test-chmtime.c static int timespec_arg(const char *arg, long int *set_time, int *set_eq) · line 92
Quoted 10 lines
t/helper/test-chmtime.c +4
-0@@ -68,40 +71,37 @@ static int timespec_arg(const char *arg, long int *set_time, int *set_eq)int cmd__chmtime (int argc,const char **argv) { + argc =parse_options (argc, argv,NULL , options, chmtime_usage, +PARSE_OPT_KEEP_UNKNOWN_OPT ); +if (argc <1 || (!get && !verbose && argc <2 )) +usage_with_options (chmtime_usage, options);
die_for_incompatible_opt2(get, "--get", verbose, "--verbose");
Quoted 6 lines
t/helper/test-chmtime.c +0
-0if (timespec_arg (argv[i], &set_time, &set_eq)) { ++i; }base-commit:
7e34d8174dcda3435d1404c9a30bef46afe6c13e
Quoted 7 lines · Junio C Hamano
Quoted 3 lines · Shashank S via GitGitGadget
t/helper/test-chmtime.c | 46 +++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 25 deletions(-)
Quoted 9 lines
Forgot to mention one thing. Do we have any tests to cover the behaviour of this test helper? If not, should we add some (not a rhetorical question)?
Also, one thing I missed is that the original ignored --verbose when --get was given, since they contradict with each other. Shouldn't we make them explicitly incompatible with each other, perhaps before where we call
timespec_arg()?
This is a great catch. While test-chmtime is heavily utilized in scripts like t5304-prune.sh, there are no tests covering the helper's actual argument parsing.
In my v2, I will add die_for_incompatible_opt2() immediately after parse_options(), and I will include a second patch in the series that adds a dedicated t/t00xx-test-chmtime.sh script to explicitly test the --get/--verbose incompatibility and the standard usage.
Thanks again for the excellent guidance
Shashank s mogaveera
PATCH v2 1/1 test-chmtime: use parse_options() to parse command-line flags
In reply to Shashank S via GitGitGadgetFrom: Shashank S Mogaveera <[email protected]>
cmd__chmtime() currently parses the '-v'/'--verbose' and '-g'/'--get' flags manually using strcmp() checks and manual index tracking.
Migrate cmd__chmtime() to use parse_options() and usage_with_options() instead. Pass PARSE_OPT_KEEP_UNKNOWN_OPT so that negative relative time offsets (such as '-86400') are not rejected as unknown short options and remain in argv for timespec_arg() to parse.
Additionally, strictly prevent combining mutually exclusive options ('--get' and '--verbose') by utilizing die_for_incompatible_opt2(), and introduce a new test script to verify both this restriction and the standard usage.
---
[Outreachy]test-chmtime: use parse_options() to parse command-line flags
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2280%2Fshashu8660%2Foutreachy3-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2280/shashu8660/outreachy3-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2280
Range-diff vs v1:
1: 4f702e3f0b ! 1: 43c96de13e test-chmtime: use parse_options() to parse command-line flags
@@ Commit message
cmd__chmtime() currently parses the '-v'/'--verbose' and '-g'/'--get' flags manually using strcmp() checks and manual index tracking.
- Refactor cmd__chmtime() to use parse_options() and usage_with_options()
+ Migrate cmd__chmtime() to use parse_options() and usage_with_options()
instead. Pass PARSE_OPT_KEEP_UNKNOWN_OPT so that negative relative time offsets (such as '-86400') are not rejected as unknown short options and remain in argv for timespec_arg() to parse.
+ Additionally, strictly prevent combining mutually exclusive options
+ ('--get' and '--verbose') by utilizing die_for_incompatible_opt2(),
+ and introduce a new test script to verify both this restriction and
+ the standard usage.
+
## t/helper/test-chmtime.c ##
@@
+ *
+ * The mtime can be changed to an absolute value:
+ *
+- * test-tool chmtime =<seconds> file...
++ * test-tool chmtime =<seconds> file...
+ *
+ * Relative to the current time as returned by time(3):
+ *
+- * test-tool chmtime =+<seconds> (or =-<seconds>) file...
++ * test-tool chmtime =+<seconds> (or =-<seconds>) file...
+ *
+ * Or relative to the current mtime of the file:
+ *
+- * test-tool chmtime <seconds> file...
+- * test-tool chmtime +<seconds> (or -<seconds>) file...
++ * test-tool chmtime <seconds> file...
++ * test-tool chmtime +<seconds> (or -<seconds>) file...
+ *
+ * Examples:
+ *
+ * To print the mtime and the file name use --verbose and set
+ * the file mtime offset to 0:
+ *
+- * test-tool chmtime -v +0 file
++ * test-tool chmtime -v +0 file
+ *
+ * To print only the mtime use --get:
+ *
+- * test-tool chmtime --get file
++ * test-tool chmtime --get file
+ *
+ * To set the mtime to current time:
+ *
+- * test-tool chmtime =+0 file
++ * test-tool chmtime =+0 file
+ *
+ * To set the file mtime offset to +1 and print the new value:
+ *
+- * test-tool chmtime --get +1 file
++ * test-tool chmtime --get +1 file
+ *
*/
#include "test-tool.h"
#include "git-compat-util.h"
@@ t/helper/test-chmtime.c
-static const char usage_str[] =
- "(-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>...";
+static const char * const chmtime_usage[] = {
-+ "test-tool chmtime (-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>...",
++ "test-tool chmtime [<options>] (+|=|=+|=-|-)<seconds> <file>...",
+ NULL
+};
static int timespec_arg(const char *arg, long int *set_time, int *set_eq)
{
-@@ t/helper/test-chmtime.c: static int timespec_arg(const char *arg, long int *set_time, int *set_eq) -
- int cmd__chmtime(int argc, const char **argv)
- {
-- static int verbose;
-- static int get;
-+ int verbose = 0;
-+ int get = 0;
+@@ t/helper/test-chmtime.c: int cmd__chmtime(int argc, const char **argv)
+ static int verbose;
+ static int get;
- int i = 1;
+ int i = 0;
@@ t/helper/test-chmtime.c: static int timespec_arg(const char *arg, long int *set_
- if (i == argc) {
- goto usage;
- }
-+ if (argc < 1 || (!get && !verbose && argc < 2))
++ die_for_incompatible_opt2(get, "--get", verbose, "--verbose");
++
++ if (argc < 1)
+ usage_with_options(chmtime_usage, options);
if (timespec_arg(argv[i], &set_time, &set_eq)) {
- ++i;
- } else {
- if (get == 0 && verbose == 0) {
- fprintf(stderr, "Not a base-10 integer: %s\n", argv[i] + 1);
+- ++i;
+- } else {
+- if (get == 0 && verbose == 0) {
+- fprintf(stderr, "Not a base-10 integer: %s\n", argv[i] + 1);
- goto usage;
-+ usage_with_options(chmtime_usage, options);
- }
+- }
++ i++;
++ } else if (!get && !verbose) {
++ fprintf(stderr, "Not a base-10 integer: %s\n", argv[i] + 1);
++ usage_with_options(chmtime_usage, options);
}
if (i == argc)
@@ t/helper/test-chmtime.c: int cmd__chmtime(int argc, const char **argv)
continue;
}
+ #ifdef GIT_WINDOWS_NATIVE
+ if (!(sb.st_mode & S_IWUSR) &&
+- chmod(argv[i], sb.st_mode | S_IWUSR)) {
++ chmod(argv[i], sb.st_mode | S_IWUSR)) {
+ fprintf(stderr, "Could not make user-writable %s: %s",
+ argv[i], strerror(errno));
+ return 1;
+@@ t/helper/test-chmtime.c: int cmd__chmtime(int argc, const char **argv)
+ utb.actime = sb.st_atime;
+ utb.modtime = set_eq ? set_time : sb.st_mtime + set_time;
+
+- mtime = utb.modtime < 0 ? 0: utb.modtime;
++ mtime = utb.modtime < 0 ? 0 : utb.modtime;
+ if (get) {
+ printf("%"PRIuMAX"\n", mtime);
+ } else if (verbose) {
@@ t/helper/test-chmtime.c: int cmd__chmtime(int argc, const char **argv)
}
#endif
@@ t/helper/test-chmtime.c: int cmd__chmtime(int argc, const char **argv)
- fprintf(stderr, "usage: %s %s\n", argv[0], usage_str);
- return 1;
}
+
+ ## t/meson.build ##
+@@ t/meson.build: integration_tests = [
+ 't0071-sort.sh',
+ 't0080-unit-test-output.sh',
+ 't0081-find-pack.sh',
++ 't0082-test-chmtime.sh',
+ 't0090-cache-tree.sh',
+ 't0091-bugreport.sh',
+ 't0092-diagnose.sh',
+
+ ## t/t0082-test-chmtime.sh (new) ##
+@@
++#!/bin/sh
++
++test_description='tests for test-tool chmtime'
++
++TEST_PASSES_SANITIZE_LEAK=true
++. ./test-lib.sh
++
++test_expect_success 'setup' '
++ echo hello >file
++'
++
++test_expect_success 'mutually exclusive options are rejected' '
++ test_must_fail test-tool chmtime --get --verbose +0 file 2>err &&
++ test_grep "cannot be used together" err
++'
++
++test_expect_success 'missing timespec without -g or -v is rejected' '
++ test_must_fail test-tool chmtime file 2>err &&
++ test_grep "usage: test-tool chmtime" err
++'
++
++test_done
t/helper/test-chmtime.c | 72 +++++++++++++++++++---------------------- t/meson.build | 1 + t/t0082-test-chmtime.sh | 22 +++++++++++++ 3 files changed, 57 insertions(+), 38 deletions(-) create mode 100755 t/t0082-test-chmtime.sh
t/helper/test-chmtime.c +34 -38
@@ -5,43 +5,46 @@* * The mtime can be changed to an absolute value: * -* test-tool chmtime =<seconds> file... +* test-tool chmtime =<seconds> file... * * Relative to the current time as returned by time(3): * -* test-tool chmtime =+<seconds> (or =-<seconds>) file... +* test-tool chmtime =+<seconds> (or =-<seconds>) file... * * Or relative to the current mtime of the file: * -* test-tool chmtime <seconds> file... -* test-tool chmtime +<seconds> (or -<seconds>) file... +* test-tool chmtime <seconds> file... +* test-tool chmtime +<seconds> (or -<seconds>) file... * * Examples: * * To print the mtime and the file name use --verbose and set * the file mtime offset to 0: * -* test-tool chmtime -v +0 file +* test-tool chmtime -v +0 file * * To print only the mtime use --get: * -* test-tool chmtime --get file +* test-tool chmtime --get file * * To set the mtime to current time: * -* test-tool chmtime =+0 file +* test-tool chmtime =+0 file * * To set the file mtime offset to +1 and print the new value: * -* test-tool chmtime --get +1 file +* test-tool chmtime --get +1 file * */ #include "test-tool.h" #include "git-compat-util.h" +#include "parse-options.h" #include <utime.h> -static const char usage_str[] = -"(-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>..." ; +static const char *const chmtime_usage[] = { +"test-tool chmtime [<options>] (+|=|=+|=-|-)<seconds> <file>..." , +NULL +};static int timespec_arg (const char *arg,long int *set_time,int *set_eq) {
@@ -71,37 +74,34 @@ int cmd__chmtime(int argc, const char **argv)static int verbose;static int get; -int i =1 ; +int i =0 ;/* no mtime change by default */ int set_eq =0 ;long int set_time =0 ; -if (argc <3 ) - goto usage; +struct option options[] = { +OPT_BOOL ('v' ,"verbose" , &verbose,"print the mtime and the file name" ), +OPT_BOOL ('g' ,"get" , &get,"print only the mtime" ), +OPT_END () + }; -if (strcmp (argv[i],"--get" ) ==0 ||strcmp (argv[i],"-g" ) ==0 ) { - get =1 ; - ++i; - }else if (strcmp (argv[i],"--verbose" ) ==0 ||strcmp (argv[i],"-v" ) ==0 ) { - verbose =1 ; - ++i; - } + argc =parse_options (argc, argv,NULL , options, chmtime_usage, +PARSE_OPT_KEEP_UNKNOWN_OPT ); -if (i == argc) { - goto usage; - } +die_for_incompatible_opt2 (get,"--get" , verbose,"--verbose" ); + +if (argc <1 ) +usage_with_options (chmtime_usage, options);if (timespec_arg (argv[i], &set_time, &set_eq)) { - ++i; - }else { -if (get ==0 && verbose ==0 ) { -fprintf (stderr,"Not a base-10 integer: %s\n" , argv[i] +1 ); - goto usage; - } + i++; + }else if (!get && !verbose) { +fprintf (stderr,"Not a base-10 integer: %s\n" , argv[i] +1 ); +usage_with_options (chmtime_usage, options); }if (i == argc) - goto usage; +usage_with_options (chmtime_usage, options);for (; i < argc; i++) {struct stat sb;
@@ -110,13 +110,13 @@ int cmd__chmtime(int argc, const char **argv)if (stat (argv[i], &sb) <0 ) {fprintf (stderr,"Failed to stat %s: %s. Skipping\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));continue ; }#ifdef GIT_WINDOWS_NATIVE if (!(sb.st_mode &S_IWUSR ) && -chmod (argv[i], sb.st_mode |S_IWUSR )) { +chmod (argv[i], sb.st_mode |S_IWUSR )) {fprintf (stderr,"Could not make user-writable %s: %s" , argv[i],strerror (errno));return 1 ;
@@ -126,7 +126,7 @@ int cmd__chmtime(int argc, const char **argv) utb.actime = sb.st_atime; utb.modtime = set_eq ? set_time : sb.st_mtime + set_time; - mtime = utb.modtime <0 ?0 : utb.modtime; + mtime = utb.modtime <0 ?0 : utb.modtime;if (get) {printf ("%" PRIuMAX"\n" , mtime); }else if (verbose) {
@@ -150,14 +150,10 @@ int cmd__chmtime(int argc, const char **argv) }#endif fprintf (stderr,"Failed to modify time on %s: %s\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));return 1 ; } }return 0 ; - -usage: -fprintf (stderr,"usage: %s %s\n" , argv[0 ], usage_str); -return 1 ; }
t/meson.build +1 -0
@@ -128,6 +128,7 @@ integration_tests = [ 't0071-sort.sh', 't0080-unit-test-output.sh', 't0081-find-pack.sh', + 't0082-test-chmtime.sh', 't0090-cache-tree.sh', 't0091-bugreport.sh', 't0092-diagnose.sh',
t/t0082-test-chmtime.sh new file +22 -0
@@ -0,0 +1,22 @@ +#!/bin/sh + +test_description='tests for test-tool chmtime' + +TEST_PASSES_SANITIZE_LEAK=true +. ./test-lib.sh + +test_expect_success 'setup' ' + echo hello >file +' + +test_expect_success 'mutually exclusive options are rejected' ' + test_must_fail test-tool chmtime --get --verbose +0 file 2>err && + test_grep "cannot be used together" err +' + +test_expect_success 'missing timespec without -g or -v is rejected' ' + test_must_fail test-tool chmtime file 2>err && + test_grep "usage: test-tool chmtime" err +' + +test_done
base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e
Signature · 1 line
gitgitgadget
Hi Shashank,
I built v2 (43c96de13e) with NO_RUST=1 and compared it with a build of the base commit (7e34d8174d). t0082 passes 3 of 3. Thanks for taking Junio's three suggestions from v1.
Quoted 3 lines
instead. Pass
PARSE_OPT_KEEP_UNKNOWN_OPTso that negative relative time offsets (such as '-86400') are not rejected as unknown short options and remain in argv fortimespec_arg()to parse.
No test covers this. With that argument set to 0, t0082 still passed 3 of 3, while "test-tool chmtime --get -86400 file" failed with "unknown switch `8'" (exit 129). Could you add a test like this one? It passes with v2 and fails without the flag:
test_expect_success 'negative offset is not taken as an option' '
test-tool chmtime =1000000000 file &&
test-tool chmtime --get -86400 file >actual && echo 999913600 >expect &&
test_cmp expect actual
'
Compared with the base build, I also saw three changes that the commit message does not mention:
- "+0 --get f": base tried to stat "--get"; v2 accepts the flag after the timespec and prints the mtime.
- "-g -v f": base tried to stat "-v" [and still printed the mtime]
and exited 0; v2 dies (exit 128).
- usage errors exit with 129 instead of 1.
Could you mention them in the commit message, or let me know if any of them is not intended?
Cynthia
On Mon, 2 Nov 2026 at 08:17, Cynthia Uche <[email protected]> wrote:
Hi Cynthia,
Thank you so much for taking the time to test and review this
Quoted 7 lines
Hi Shashank
No test covers this. With that argument set to 0, t0082 still passed 3 of 3, while "test-tool chmtime --get -86400 file" failed with "unknown switch `8'" (exit 129). Could you add a test like this one? It passes with v2 and fails without the flag:
Quoted 7 lines
test_expect_success'negative offset is not taken as an option' 'test-tool chmtime =1000000000 file && test-tool chmtime --get -86400 file >actual && echo 999913600 >expect && test_cmp expect actual'
This is an excellent catch. Adding a dedicated test to prevent a regression here is a great idea, and I appreciate you providing the snippet. I will include this test in the v3 iteration.
Quoted 12 lines
Compared with the base build, I also saw three changes that the commit message does not mention:
- "+0 --get f": base tried to stat "--get"; v2 accepts the flag after the timespec and prints the mtime.
- "-g -v f": base tried to stat "-v" [and still printed the mtime]and exited 0; v2 dies (exit 128).- usage errors exit with 129 instead of 1.
Could you mention them in the commit message, or let me know if any of them is not intended?
All of these behavioral shifts are intended! Since parse_options() handles arguments independent of their position and uses standard error codes (129 for usage, 128 for die), the tool now behaves much more like a standard Git command. You are completely right that I should document these side-effects explicitly in the commit message.
I will add a breakdown of these behavioral improvements to the commit message in v3. Thanks again for the meticulous review!
Shashank S Mogaveera
PATCH v3 1/1 test-chmtime: use parse_options() to parse command-line flags
In reply to Shashank S via GitGitGadgetFrom: Shashank S Mogaveera <[email protected]>
cmd__chmtime() currently parses the '-v'/'--verbose' and '-g'/'--get' flags manually using strcmp() checks and manual index tracking.
Migrate cmd__chmtime() to use parse_options() and usage_with_options() instead. Pass PARSE_OPT_KEEP_UNKNOWN_OPT so that negative relative time offsets (such as '-86400') are not rejected as unknown short options and remain in argv for timespec_arg() to parse.
Additionally, strictly prevent combining mutually exclusive options ('--get' and '--verbose') by utilizing die_for_incompatible_opt2(), and introduce a new test script to verify both this restriction and the standard usage.
This migration also introduces the following intended behavioral improvements over the original manual parsing:
- Position-independent flags: Flags can now be safely passed after the timespec (e.g., +0 --get file now works, whereas it previously tried to stat a file literally named --get).
- Incompatible flags strictly abort: Passing both -g and -v now triggers a fatal error (exit 128), whereas it previously tried to stat -v and exited 0.
- Standardized error codes: Usage errors now properly exit with 129 instead of 1.
---
[Outreachy]test-chmtime: use parse_options() to parse command-line flags
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2280%2Fshashu8660%2Foutreachy3-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2280/shashu8660/outreachy3-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/2280
Range-diff vs v2:
1: 43c96de13e ! 1: 512b17b39a test-chmtime: use parse_options() to parse command-line flags
@@ Commit message
and introduce a new test script to verify both this restriction and the standard usage.
+ This migration also introduces the following intended behavioral
+ improvements over the original manual parsing:
+ - Position-independent flags: Flags can now be safely passed after the
+ timespec (e.g., +0 --get file now works, whereas it previously tried
+ to stat a file literally named --get).
+ - Incompatible flags strictly abort: Passing both -g and -v now
+ triggers a fatal error (exit 128), whereas it previously tried to stat
+ -v and exited 0.
+ - Standardized error codes: Usage errors now properly exit with 129
+ instead of 1.
+
## t/helper/test-chmtime.c ##
@@ t/t0082-test-chmtime.sh (new)
+ test_grep "usage: test-tool chmtime" err
+'
+
++test_expect_success 'negative offset is not taken as an option' '
++ test-tool chmtime =1000000000 file &&
++ test-tool chmtime --get -86400 file >actual &&
++ echo 999913600 >expect &&
++ test_cmp expect actual
++'
++
+test_done
t/helper/test-chmtime.c | 72 +++++++++++++++++++---------------------- t/meson.build | 1 + t/t0082-test-chmtime.sh | 29 +++++++++++++++++ 3 files changed, 64 insertions(+), 38 deletions(-) create mode 100755 t/t0082-test-chmtime.sh
t/helper/test-chmtime.c +34 -38
@@ -5,43 +5,46 @@* * The mtime can be changed to an absolute value: * -* test-tool chmtime =<seconds> file... +* test-tool chmtime =<seconds> file... * * Relative to the current time as returned by time(3): * -* test-tool chmtime =+<seconds> (or =-<seconds>) file... +* test-tool chmtime =+<seconds> (or =-<seconds>) file... * * Or relative to the current mtime of the file: * -* test-tool chmtime <seconds> file... -* test-tool chmtime +<seconds> (or -<seconds>) file... +* test-tool chmtime <seconds> file... +* test-tool chmtime +<seconds> (or -<seconds>) file... * * Examples: * * To print the mtime and the file name use --verbose and set * the file mtime offset to 0: * -* test-tool chmtime -v +0 file +* test-tool chmtime -v +0 file * * To print only the mtime use --get: * -* test-tool chmtime --get file +* test-tool chmtime --get file * * To set the mtime to current time: * -* test-tool chmtime =+0 file +* test-tool chmtime =+0 file * * To set the file mtime offset to +1 and print the new value: * -* test-tool chmtime --get +1 file +* test-tool chmtime --get +1 file * */ #include "test-tool.h" #include "git-compat-util.h" +#include "parse-options.h" #include <utime.h> -static const char usage_str[] = -"(-v|--verbose|-g|--get) (+|=|=+|=-|-)<seconds> <file>..." ; +static const char *const chmtime_usage[] = { +"test-tool chmtime [<options>] (+|=|=+|=-|-)<seconds> <file>..." , +NULL +};static int timespec_arg (const char *arg,long int *set_time,int *set_eq) {
@@ -71,37 +74,34 @@ int cmd__chmtime(int argc, const char **argv)static int verbose;static int get; -int i =1 ; +int i =0 ;/* no mtime change by default */ int set_eq =0 ;long int set_time =0 ; -if (argc <3 ) - goto usage; +struct option options[] = { +OPT_BOOL ('v' ,"verbose" , &verbose,"print the mtime and the file name" ), +OPT_BOOL ('g' ,"get" , &get,"print only the mtime" ), +OPT_END () + }; -if (strcmp (argv[i],"--get" ) ==0 ||strcmp (argv[i],"-g" ) ==0 ) { - get =1 ; - ++i; - }else if (strcmp (argv[i],"--verbose" ) ==0 ||strcmp (argv[i],"-v" ) ==0 ) { - verbose =1 ; - ++i; - } + argc =parse_options (argc, argv,NULL , options, chmtime_usage, +PARSE_OPT_KEEP_UNKNOWN_OPT ); -if (i == argc) { - goto usage; - } +die_for_incompatible_opt2 (get,"--get" , verbose,"--verbose" ); + +if (argc <1 ) +usage_with_options (chmtime_usage, options);if (timespec_arg (argv[i], &set_time, &set_eq)) { - ++i; - }else { -if (get ==0 && verbose ==0 ) { -fprintf (stderr,"Not a base-10 integer: %s\n" , argv[i] +1 ); - goto usage; - } + i++; + }else if (!get && !verbose) { +fprintf (stderr,"Not a base-10 integer: %s\n" , argv[i] +1 ); +usage_with_options (chmtime_usage, options); }if (i == argc) - goto usage; +usage_with_options (chmtime_usage, options);for (; i < argc; i++) {struct stat sb;
@@ -110,13 +110,13 @@ int cmd__chmtime(int argc, const char **argv)if (stat (argv[i], &sb) <0 ) {fprintf (stderr,"Failed to stat %s: %s. Skipping\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));continue ; }#ifdef GIT_WINDOWS_NATIVE if (!(sb.st_mode &S_IWUSR ) && -chmod (argv[i], sb.st_mode |S_IWUSR )) { +chmod (argv[i], sb.st_mode |S_IWUSR )) {fprintf (stderr,"Could not make user-writable %s: %s" , argv[i],strerror (errno));return 1 ;
@@ -126,7 +126,7 @@ int cmd__chmtime(int argc, const char **argv) utb.actime = sb.st_atime; utb.modtime = set_eq ? set_time : sb.st_mtime + set_time; - mtime = utb.modtime <0 ?0 : utb.modtime; + mtime = utb.modtime <0 ?0 : utb.modtime;if (get) {printf ("%" PRIuMAX"\n" , mtime); }else if (verbose) {
@@ -150,14 +150,10 @@ int cmd__chmtime(int argc, const char **argv) }#endif fprintf (stderr,"Failed to modify time on %s: %s\n" , - argv[i],strerror (errno)); + argv[i],strerror (errno));return 1 ; } }return 0 ; - -usage: -fprintf (stderr,"usage: %s %s\n" , argv[0 ], usage_str); -return 1 ; }
t/meson.build +1 -0
@@ -128,6 +128,7 @@ integration_tests = [ 't0071-sort.sh', 't0080-unit-test-output.sh', 't0081-find-pack.sh', + 't0082-test-chmtime.sh', 't0090-cache-tree.sh', 't0091-bugreport.sh', 't0092-diagnose.sh',
t/t0082-test-chmtime.sh new file +29 -0
@@ -0,0 +1,29 @@ +#!/bin/sh + +test_description='tests for test-tool chmtime' + +TEST_PASSES_SANITIZE_LEAK=true +. ./test-lib.sh + +test_expect_success 'setup' ' + echo hello >file +' + +test_expect_success 'mutually exclusive options are rejected' ' + test_must_fail test-tool chmtime --get --verbose +0 file 2>err && + test_grep "cannot be used together" err +' + +test_expect_success 'missing timespec without -g or -v is rejected' ' + test_must_fail test-tool chmtime file 2>err && + test_grep "usage: test-tool chmtime" err +' + +test_expect_success 'negative offset is not taken as an option' ' + test-tool chmtime =1000000000 file && + test-tool chmtime --get -86400 file >actual && + echo 999913600 >expect && + test_cmp expect actual +' + +test_done
base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e
Signature · 1 line
gitgitgadget
Hi Shashank,
I applied v3 on master (fb255012ac). t0082 passes 4 of 4, and the negative-offset test fails if I set the PARSE_OPT_KEEP_UNKNOWN_OPT argument to 0, so it guards that flag. The behaviour changes listed in the commit message match what I see against a build of master.
Looks good. Thanks for adding the test and the commit message text.
Signature · 1 line
Cynthia