From f782fbbbbc3080a33be56c137843260f00be36bd Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Tue, 1 Dec 2020 14:23:13 +0100 Subject: [PATCH 1/1] Initial commit Signed-off-by: Frantisek Hrbata --- COPYING | 14 ++ Documentation/git-mr-create.txt | 81 ++++++ Documentation/git-mr-fetch.txt | 78 ++++++ Documentation/git-mr-mbox.txt | 125 ++++++++++ Documentation/git-mr-mbox.txt.bak | 125 ++++++++++ Documentation/git-mr-push.txt | 70 ++++++ Documentation/git-mr-remove.txt | 67 +++++ Documentation/git-mr-sendmail.txt | 51 ++++ Documentation/git-mr-show.txt | 158 ++++++++++++ Documentation/git-mr-tag.txt | 99 ++++++++ Documentation/git-mr-update.txt | 35 +++ Documentation/git-mr.txt | 82 ++++++ Makefile | 84 +++++++ gimrr.spec | 28 +++ git-mr-create | 114 +++++++++ git-mr-fetch | 88 +++++++ git-mr-mbox | 86 +++++++ git-mr-push | 79 ++++++ git-mr-remove | 67 +++++ git-mr-sendmail | 102 ++++++++ git-mr-show | 195 +++++++++++++++ git-mr-tag | 105 ++++++++ git-mr-update | 123 +++++++++ mrlib.py | 499 +++++++++++++++++++++++++++++++++++++ 24 files changed, 2555 insertions(+) create mode 100644 COPYING create mode 100644 Documentation/git-mr-create.txt create mode 100644 Documentation/git-mr-fetch.txt create mode 100644 Documentation/git-mr-mbox.txt create mode 100644 Documentation/git-mr-mbox.txt.bak create mode 100644 Documentation/git-mr-push.txt create mode 100644 Documentation/git-mr-remove.txt create mode 100644 Documentation/git-mr-sendmail.txt create mode 100644 Documentation/git-mr-show.txt create mode 100644 Documentation/git-mr-tag.txt create mode 100644 Documentation/git-mr-update.txt create mode 100644 Documentation/git-mr.txt create mode 100644 Makefile create mode 100644 gimrr.spec create mode 100755 git-mr-create create mode 100755 git-mr-fetch create mode 100755 git-mr-mbox create mode 100755 git-mr-push create mode 100755 git-mr-remove create mode 100755 git-mr-sendmail create mode 100755 git-mr-show create mode 100755 git-mr-tag create mode 100755 git-mr-update create mode 100755 mrlib.py diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..5a8e332 --- /dev/null +++ b/COPYING @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/Documentation/git-mr-create.txt b/Documentation/git-mr-create.txt new file mode 100644 index 0000000..cccde12 --- /dev/null +++ b/Documentation/git-mr-create.txt @@ -0,0 +1,81 @@ +GIT-MR-CREATE(1) +================ +:doctype: manpage + + +NAME +---- +git-mr-create - Create new merge request + + +SYNOPSIS +-------- +*git-mr-create* ['OPTIONS'] ['REV'] + + +DESCRIPTION +----------- +Creates a new MR (merge request) from 'REV', which is a revision in +whatever form is accepted by git. If 'REV' is not specified, 'HEAD' +is used as the default value. New MR is identified by the full 'SHA-1' of 'REV' +and a new reference is created under the 'mrs' refs namespace +'refs/mrs///head'. + +Each MR has to have a base specified with the *-b* argument. It's a 'REV' the +MR is based on. + +Description for the new MR is taken from *-m*, *-F* arguments or from *-* +'stdin'. The description is in the same format as for 'git-commit'. If none +of these options is used, editor is started by default. + + +OPTIONS +------- +*-b, --base* 'REV':: + Base revision of the MR. + +*-, --stdin*:: + Read MR description from 'stdin'. Description entered via *-m* or *-F* + option is ignored. + +*-f, --file* 'FILE':: + Use MR description from 'FILE'. Description entered via *-m* option is + ignored. + +*-m, --message 'MSG':: + Use 'MSG' as MR description. Multiple 'MSG's are concatenated as separate + paragraphs. + +EXAMPLES +-------- +Create a new MR based on 'master' branch. + +`$ git checkout -b feature master`:: + Checkout a new branch 'feature' based on 'master' and add your changes. + +`$ git mr-create -b master -m "My new feature"`:: + Create a new MR from the 'feature' branch based on 'master'. + +`$ git mr-show`:: + Show some basic info about the newly created MR. 'git-mr-show' can display + all sort of information in a user specific formats. + +`$ git mr-tag -A -a -t bz -v 12345`:: + Attach 'bz' tag to all commits in MR. Tags are dynamic and don't + have to be predefined anywhere. Once tag is added, it can be + displayed along with other information with 'git-mr-show'. + +`$ git mr-tag -a -t N -v "This is my MR note"`:: + Attach 'N' tag with note to MR only. + +`$ git mr-show -c --cfmt="{bz} {h} {s}"`:: + Display commits for MR in a specific format with + the 'bz' tag included. + +`$ git mr-push"`:: + If all is as expected, push MR for review to remote. + + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr-fetch.txt b/Documentation/git-mr-fetch.txt new file mode 100644 index 0000000..cf80e29 --- /dev/null +++ b/Documentation/git-mr-fetch.txt @@ -0,0 +1,78 @@ +GIT-MR-FETCH(1) +=============== +:doctype: manpage + + +NAME +---- +git-mr-fetch - Download merge request(s), tags and mboxes from remote + + +SYNOPSIS +-------- +*git-mr-fetch* ['OPTIONS'] ['REV' ['REV' ...]] + + +DESCRIPTION +----------- +This is just a wrapper around 'git-fetch', which downloads refs in the 'mrs' +namespace from remote. By default only refs in other user namespaces are +fetched. This can be changed by the *-f* option, but all local changes in +your namespace will be lost. + +If no 'REV' is specified, 'HEAD' is used as the default MR. Otherwise +'REV's specified as positional arguments or on *-* stdin are used. The *-A* +option can be used to fetch new remote refs and to update all local refs, +except for the ones owned by you. + +By default all three types of refs are downloaded. This can be changed by +*--head*, *--tags* and *--mbox* options. + + +OPTIONS +------- +*-A, --all*:: + Fetch all MR refs from remote, except for yours. To fully sync + with remote use the *-f* option, but note that this will overwrite any + local(unpushed) changes you may have. MRs entered as positional arguments or + on *-* stdin are ignored. + +*-, --stdin*:: + Read MRs from stdin. MRs entered as positional arguments are ignored. + +*-r, --remote* 'REMOTE':: + The remote repository to fetch from, default is origin. + +*--head*:: + Fetch MR commits. These are head refs in the 'mrs' + namespace. + +*--tags*:: + Fetch tags refs. These are tags refs in the 'mrs' namespace. + +*--mbox*:: + Fetch mboxes refs. These are mbox refs in the 'mrs' namespace. + +*-f, --force*:: + Fetch also refs your refs. All local changes will be lost! + +*-q, --quiet*:: + Progress is not reported to the standard error stream. + +EXAMPLES +-------- +`$ git mr-fetch`:: + Fetch refs for current MR specified by 'HEAD'. + +`$ git mr-fetch -A`:: + Fetch refs for all MRs from origin remote. + +`$ git mr-fetch -A -r upstream`:: + Fetch refs for all MRs from upstream remote. + +`$ git mr-fetch -A -r upstream --tags --mbox`:: + Same as previous, but fetch only tags and mboxes. + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr-mbox.txt b/Documentation/git-mr-mbox.txt new file mode 100644 index 0000000..47a18dc --- /dev/null +++ b/Documentation/git-mr-mbox.txt @@ -0,0 +1,125 @@ +GIT-MR-MBOX(1) +============== +:doctype: manpage + + +NAME +---- +git-mr-mbox - Generate mbox for merge request(s) and print it to stdout + + +SYNOPSIS +-------- +*git-mr-mbox* ['OPTIONS'] ['MR' ['MR' ...]] + + +DESCRIPTION +----------- +This command, if used in combination with 'git-mr-fetch', 'git-mr-push' +and 'git-mr-sendmail' provides a possibility to do MR reviews in email +based form, using your favorite MUA like Mutt. It generates mbox file +from 'head' and 'mbox' refs stored in the 'mrs' refs namespace. This +replaces fetching mail from SMTP server and mbox is generated from data +stored in git only. + +Please check the *MUTT* section to see one of possible and basic +setups. + +OPTIONS +------- +*-A, --all*:: + Generate mbox for all MRs. MRs entered as positional arguments or + on *-* stdin are ignored. + +*-, --stdin*:: + Read MRs from stdin. MRs entered as positional arguments are ignored. + +*-r, --read-refs* 'FILE':: + Read refs from 'FILE'. If used, mbox is generated only for refs not + presented in 'FILE'. This can be used to generate mbox for new MRs and + new comments only, simulating fetching of new mail from SMTP server. + +*-w, --write-refs* 'FILE':: + Write refs for MRs and comments in the generated mbox to a 'FILE'. It + can be the same 'FILE' as for the *-r* option. You can incrementally + update the mbox by specifying both *-r* and *-w* options and using the + same 'FILE.' Only newly added or updated refs are changed. + +*-m, --mbox* 'FILE':: + Append mbox to 'FILE' instead of writing it to stdout. + + +MUTT +---- +This section provides basic Mutt setup for MR reviews. + +Mutt config file that can be used. For the purpose +of this example, please store it as '~/.gimrr/muttrc'. +---- +set realname = "your name" +set from = "your email address" +set sendmail = "git mr-sendmail" +set sort = threads +set timeout = 10 +---- + +Add the following aliases to your git config. +---- +mr-mutt = "!sh -c 'dir=${HOME}/.gimrr; mkdir -p $dir && \ + mr=$(git rev-parse --verify ${1:-HEAD}) && \ + git mr-mbox -m ${dir}/${mr}.mbox -r ${dir}/${mr}.refs -w ${dir}/${mr}.refs $mr \ + && mutt -f ${dir}/${mr}.mbox -F ${dir}/muttrc'" git-mr-review + +mr-mutt-all = "!sh -c 'dir=${HOME}/.gimrr; mkdir -p $dir && \ + git mr-mbox -A -m ${dir}/all.mbox -r ${dir}/all.refs -w ${dir}/all.refs && \ + mutt -f ${dir}/all.mbox -F ${dir}/muttrc'" git-mr-review-all +---- + +The 'git-mr-mutt' alias will generate mbox and start Mutt for current +MR(HEAD). You can pass it MR sha1 as argument to review a different MR. +The 'git-mr-mutt-all' alias generates mbox for all MRs +in the current git repo. You can also easily reply to patches or comments +from others from within git, because 'git-mr-sendmail' is set for mail +delivery. + +Note that your replies and MRs are created locally and you need to push them. +The same goes for changes made by others. You need to fetch them. Hence +you should run 'git mr-fetch -A' and 'git mr-push -A --mbox' at some point. + +You can use something like following in cron job or loop to simulate mail +synchronization for all MRs in specific repo. +---- +REPO="path to git repo" +NAME="$(basename $REPO)" +DIR="${HOME}/.gimrr" +git -C $REPO mr-fetch -A +git -C $REPO mr-push -A --mbox +git -C mr-mbox -A -m ${DIR}/${NAME}.mbox -r ${DIR}/${NAME}.refs -w ${DIR}/${NAME}.refs +---- + +EXAMPLES +-------- +`$ git mr-mbox MR1 MR2`:: + Generate mbox for MR1 and MR2 to stdout. + +`$ git mr-mbox`:: + Generate mbox for current MR(HEAD) to stdout. + +`$ git mr-mbox -m mbox`:: + Same as above but store the mbox to 'mbox' file. + +`$ git mr-mbox -A -m mbox`:: + Generate 'mbox' file for all available MRs and their comments. + +`$ git mr-mbox -A -m mbox -r mbox.refs -w mbox.refs`:: + Same as above, but also reads and stores refs for which + the 'mbox' file was generated. It can be called repeatedly + and only new MRs and comments will be appended to mbox. + Note the this should be used along with 'git-mr-fetch' to + actually get the changes from server, even though your unpushed + MRs and comments will be available right away. + + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr-mbox.txt.bak b/Documentation/git-mr-mbox.txt.bak new file mode 100644 index 0000000..8f7a6ad --- /dev/null +++ b/Documentation/git-mr-mbox.txt.bak @@ -0,0 +1,125 @@ +GIT-MR-MBOX(1) +============== +:doctype: manpage + + +NAME +---- +git-mr-mbox - Generate mbox for merge request(s) and print it to stdout + + +SYNOPSIS +-------- +*git-mr-mbox* ['OPTIONS'] ['MR' ['MR' ...]] + + +DESCRIPTION +----------- +This command, if used in combination with 'git-mr-fetch', 'git-mr-push' +and 'git-mr-sendmail' provides a possibility to do MR reviews in email +based form, using your favorite MUA like Mutt. It generates mbox file +from 'head' and 'mbox' refs stored in the 'mrs' refs namespace. This +replaces fetching mail from SMTP server and mbox is generated from data +stored in git only. + +Please check the *MUTT* section to see one of possible and basic +setups. + +OPTIONS +------- +*-A, --all*:: + Generate mbox for all MRs. MRs entered as positional arguments or + on *-* stdin are ignored. + +*-, --stdin*:: + Read MRs from stdin. MRs entered as positional arguments are ignored. + +*-r, --read-refs* 'FILE':: + Read refs from 'FILE'. If used, mbox is generated only for refs not + presented in 'FILE'. This can be used to generate mbox for new MRs and + new comments only, simulating fetching of new mail from SMTP server. + +*-w, --write-refs* 'FILE':: + Write refs for MRs and comments in the generated mbox to a 'FILE'. It + can be the same 'FILE' as for the *-r* option. You can incrementaly + update the mbox by specifying both *-r* and *-w* options and using the + same 'FILE.' Only newly added or updated refs are changed. + +*-m, --mbox* 'FILE':: + Append mbox to 'FILE' instead of writing it to stdout. + + +MUTT +---- +This section provides basic Mutt setup for MR reviews. + +Basic Mutt config file that can be used. For the purpose +of this example, please store it as '~/.gimrr/muttrc'. +---- +set realname = "your name" +set from = "your email address" +set sendmail = "git mr-sendmail" +set sort = threads +set timeout = 10 +---- + +Add the following aliases to your git config. +---- +mr-mutt = "!sh -c 'dir=${HOME}/.gimrr; mkdir -p $dir && \ + mr=$(git rev-parse --verify ${1:-HEAD}) && \ + git mr-mbox -m ${dir}/${mr}.mbox -r ${dir}/${mr}.refs -w ${dir}/${mr}.refs $mr \ + && mutt -f ${dir}/${mr}.mbox -F ${dir}/muttrc'" git-mr-review + +mr-mutt-all = "!sh -c 'dir=${HOME}/.gimrr; mkdir -p $dir && \ + git mr-mbox -A -m ${dir}/all.mbox -r ${dir}/all.refs -w ${dir}/all.refs && \ + mutt -f ${dir}/all.mbox -F ${dir}/muttrc'" git-mr-review-all +---- + +The 'git-mr-mutt' alias will generate mbox and start Mutt for current +MR(HEAD). You can pass it MR sha1 as argument to review a different MR. +The 'git-mr-mutt-all' alias generates mbox for all MRs +in the current git repo. You can also easily reply to patches or comments +from others from within git, because 'git-mr-sendmail' is set for mail +delivery. + +Note that your replies and MRs are created localy and you need to push them. +The same goes for changes made by others. You need to fetch them. Hence +you should run 'git mr-fetch -A' and 'git mr-push -A --mbox' at some point. + +You can use something like following in cron job or loop to simulate mail +synchronization for all MRs in specific repo. +---- +REPO="path to git repo" +NAME="myrepo" +DIR="${HOME}/.gimrr" +git -C "REPO" mr-fetch -A +git -C "REPO" mr-push -A --mbox +git -C mr-mbox -A -m $(DIR)/$(NAME).mbox -r $(DIR)/$(NAME).refs -w $(DIR)/$(NAME).refs` +---- + +EXAMPLES +-------- +`$ git mr-mbox MR1 MR2`:: + Generate mbox for MR1 and MR2 to stdout. + +`$ git mr-mbox`:: + Generate mbox for current MR(HEAD) to stdout. + +`$ git mr-mbox -m mbox`:: + Same as above but store the mbox to 'mbox' file. + +`$ git mr-mbox -A -m mbox`:: + Generate 'mbox' file for all available MRs and their comments. + +`$ git mr-mbox -A -m mbox -r mbox.refs -w mbox.refs`:: + Same as above, but also reads and stores refs for which + the 'mbox' file was generated. It can be called repeatedly + and only new MRs and comments will be appended to mbox. + Note the this should be used along with 'git-mr-fetch' to + actually get the chages from server, even though your unpushed + MRs and comments will be available right away. + + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr-push.txt b/Documentation/git-mr-push.txt new file mode 100644 index 0000000..ae79a1e --- /dev/null +++ b/Documentation/git-mr-push.txt @@ -0,0 +1,70 @@ +GIT-MR-PUSH(1) +============== +:doctype: manpage + + +NAME +---- +git-mr-push - Update remote merge request(s), tags and mboxes + + +SYNOPSIS +-------- +*git-mr-push* ['OPTIONS'] ['REV' ['REV' ...]] + + +DESCRIPTION +----------- +This is just a wrapper around 'git-push', which pushes refs in your refs +namespaces for specified 'REV's to remote. + +If no 'REV' is specified, 'HEAD' is used as the default MR. Otherwise +'REV's specified as positional arguments or on *-* stdin are used. The *-A* +option can be used to push all your local changes to remote. + +By default all three types of refs are pushed. This can be changed by +*-head*, *--tags* and *--mbox* options. + + +OPTIONS +------- +*-A, --all*:: + Push all your MR refs to remote. MRs entered as positional arguments or + on *-* stdin are ignored. + +*-, --stdin*:: + Read MRs from stdin. MRs entered as positional arguments are ignored. + +*-r, --remote* 'REMOTE':: + The remote repository to push to, default is origin. + +*--head*:: + Push MR commits. These are head refs in your 'mrs' + namespace. + +*--tags*:: + Push tags refs. These are tags refs in your 'mrs' namespace. + +*--mbox*:: + Push mboxes refs. These are mbox refs in your 'mrs' namespace. + +*-q, --quiet*:: + Progress is not reported to the standard error stream. + +EXAMPLES +-------- +`$ git mr-push`:: + Push your refs for current MR specified by 'HEAD'. + +`$ git mr-push -A`:: + Push your refs for all MRs to origin remote. + +`$ git mr-push -A -r upstream`:: + Push your refs for all MRs to upstream remote. + +`$ git mr-push -A -r upstream --tags --mbox`:: + Same as previous, but push only tags and mboxes. + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr-remove.txt b/Documentation/git-mr-remove.txt new file mode 100644 index 0000000..84b5ed1 --- /dev/null +++ b/Documentation/git-mr-remove.txt @@ -0,0 +1,67 @@ +GIT-MR-REMOVE(1) +================ +:doctype: manpage + + +NAME +---- +git-mr-remove - Remove merge request(s), tags and mboxes + + +SYNOPSIS +-------- +*git-mr-remove* ['OPTIONS'] ['REV' ['REV' ...]] + + +DESCRIPTION +----------- +Removes refs from the 'mrs' refs namespace. Only local refs are removed and +it has no effect on refs already pushed to remote. This is primary intended +to remove MRs which should not be pushed. + +If no 'REV' is specified, 'HEAD' is used as the default MR. Otherwise +'REV's specified as positional arguments or on *-* stdin are used. The *-A* +option can be used to remove refs for all MRs. + +By default all three types of refs are removed. This can be changed by +*--mrs*, *--tags* and *--mbs* options. + + +OPTIONS +------- +*-A, --all*:: + Use all available MRs as input. MRs entered as positional arguments or + on stdin are ignored. + +*-, --stdin*:: + Read MRs from stdin. MRs entered as positional arguments are ignored. + +*--mrs*:: + Remove merge request(s) refs. + +*--tags*:: + Remove tags refs. + +*--mbs*:: + Remove mboxes refs. + +*-u, --user* ['USER']:: + Remove refs for specific user(s) only. This option can be used multiple + times. If used without 'USER', just as a switch, only your refs will be + removed. + +EXAMPLES +-------- +`$ git mr-remove`:: + Remove current MR specified by 'HEAD'. + +`$ git mr-remove -A`:: + Remove all refs for all MRs. + +`$ git mr-remove -A --tags --mbs`:: + Same as previous, but remove only tags and mboxes. + + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr-sendmail.txt b/Documentation/git-mr-sendmail.txt new file mode 100644 index 0000000..a370c47 --- /dev/null +++ b/Documentation/git-mr-sendmail.txt @@ -0,0 +1,51 @@ +GIT-MR-SENDMAIL(1) +================== +:doctype: manpage + + +NAME +---- +git-mr-sendmail - Simulate sendmail behavior to send reply to MR, commit or comment + + +SYNOPSIS +-------- +*git-mr-sendmail* ['OPTIONS'] ['RECIPIENT' ['RECIPIENT' ...]] + + +DESCRIPTION +----------- +Reads mail from stdin or file and creates a new mbox entry under +'mrs' refs namespace 'refs/mrs///mbox'. To make +it visible to others you need to push your changes using 'git-mr-push'. + +The input should be normal mail reply and it has to contain *Subject*, +*To* and *In-Reply-To* headers. The *In-Reply-To* header has to contain +msgid in a format expected by 'git-mr-sendmail'. That is +`<`*mr*`.`*commit*`@localhost>`, where *mr* is full 'SHA-1' of MR and +*commit* is full 'SHA-1' of MR commit or mbox entry. Note that each +mbox entry is also identified by hash. The mbox entries and they hashes +can be seen by using e.g. 'git-log' on 'refs/mrs///mbox'. + +This command is primary intended to be used with MUAs, like Mutt, which +allow to specify a program for mail delivery. Please don't forget to +use git *-C* option if this runs outside of git repo. + +OPTIONS +------- +*-f, --file 'FILE'*:: + Read mail from FILE instead of stdin. + + +EXAMPLES +-------- +`set sendmail = "git mr-sendmail"`:: + Set this option in your Mutt configuration to Use 'git-mr-sendmail' + for mail delivery. + +`set sendmail = "git -C mr-sendmail"`:: + Set proper for git repo. + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr-show.txt b/Documentation/git-mr-show.txt new file mode 100644 index 0000000..a1b1cd7 --- /dev/null +++ b/Documentation/git-mr-show.txt @@ -0,0 +1,158 @@ +GIT-MR-SHOW(1) +============== +:doctype: manpage + + +NAME +---- +git-mr-show - Show information about merge request(s) and its commits + + +SYNOPSIS +-------- +*git-mr-show* ['OPTIONS'] ['REV' ['REV' ...]] + + +DESCRIPTION +----------- +Displays tags for MRs and their commits using Python's format string syntax. +(see *RESOURCES*). Tag names which can be used in the format, +can be listed with the *-t* option. Basic tags are created by 'git-mr-create' +and others can be added with 'git-mr-tag'. If tag is not available, it's +displayed as defined in the format string. + +MRs, for which the information is displayed, can be specified as 'REV' +positional arguments, on *-* stdin or with *-m*, *-A* option. These cannot +be mixed and have a hard-coded precedence. If no MR is specified, 'HEAD' is +used by default. Note that the MR identifier is just a full 'SHA-1' of 'REV'. + +By default only MRs information is displayed. To see commit info use *-c* option. +You can specify two format strings. One for MRs *--mfmt* and one for commits +*--cfmt*. + +In addition to Python's string format, each tag may have a sub-format and the full +tag specification is *tag*@*fmt*@*sep*@*users*`:`*pfmt*. *tag* is the tag name +as shown by the *-t* option. *fmt* is 'git-mr-show' specific format. It can +be *v* to display all tag values(this is the default) or *c* to display number +of values this tag has or *r* to display tag values along with users who set +them. *sep* is a separator to use if the tag has multiple values, default is +space. *users* is list of users, separated by comma and only values set by +these users are displayed. This has higher precedence then *-u*. *pfmt* is +Python's format specification. The '@' separator in format is mandatory if +some parts are not specified. For example if you want to display tag 'bz' +values only for user 'johndoe', you need to specify `{bz@@@johndoe}`. +Note that each tag may have multiple values set by different users. This +hopefully makes the format flexible enough to display desired information only. +Please see *EXAMPLES*. + +Sometimes it may be useful to display tags for commits in the MR format. For +example if you want to see what bugs are assigned to the MR. This can be done +by using the *-s* option. It will squash all commit tags to MR and you can use +the tag name in MR format even if there is actually no such tag attached. +Note that tags are attached to commit hash and MR has zero commit hash(`0*40`). +This is how the MR and commit tags are distinguished. The *-s* option just +assigns all tags for commits to the MR zero hash. This also requires some +caution when choosing tag names, so the MR and commit tags are not mixed +together while the *-s* option is used. As a convention MR tags uses upper case +letters. For example 'S' is subject for MR and 's' is subject for commit. + +OPTIONS +------- +*-m, --my*:: + Show information for MRs created by me. MRs entered as positional + arguments, on *-* stdin or with *-A* option are ignored. + +*-A, --all*:: + Show information for all MRs. MRs entered as positional arguments or + on *-* stdin are ignored. + +*-, --stdin*:: + Read MRs from stdin. MRs entered as positional arguments are ignored. + +*-c, --commits*:: + Show also commits information. By default only MR information is displayed. + +*-s, --squash*:: + Squash commit tags into MR. With this option all commit tags are attached + to MR and can be displayed in the MR summary. This disables *-c*. + +*--mfmt* 'FMT':: + Python's formatted string for MRs. You can specify which tags will be + displayed in MR description. Default value is + `"{M:.16} {B:.16} {CC:6.6} {S:60.60} {AN:16.16} {AE:>20.20} {AD}"`. + +*--cfmt* 'FMT':: + Python's formatted string for commits. You can specify which tags will be + displayed in commit description. Default value is + `" {h:.16} {s}"`. + +*-t, --tags*:: + List available tags for selected MRs. Use *-c* option if you want to list + tags for commits too. + +*-u, --user* ['USER']:: + Show tags for specified user(s) only. Can be used multiple times. The users + list in tag format has higher precedence. If used without 'USER', just as + a switch, only your tags will be shown. + +EXAMPLES +-------- +`$ git mr-show`:: + Show info for current MR(HEAD) using the default format. + +`$ git mr-show -c`:: + Same as previous, but also show info about each commit in the MR using the + default format. + +`$ git mr-show -c -u`:: + Same as previous, but only tag values set my me(`git config mr.user`). + +`$ git mr-show -A`:: + Show info about all MRs. + +`$ git mr-show -m -c`:: + Show info about MRs and commits created by me(`git config mr.user`). + +`$ git mr-show -c --cfmt="{bz} {h} {s}" feature`:: + Show MR info and commits info for MR identified by branch feature. + The comments format will show bugzilla, commit hash and commit + subject. + +`$ git mr-show -c --mfmt="{M} {S}" --cfmt="{bz} {h} {s}"`:: + Same as previous, but also use specific format for MR, which + will display MR hash(id) and subject. + +`$ git mr-show -c --cfmt="{bz@@|}`:: + Show commit bz tag values only and use '|' as separator. + +`$ git mr-show -c --cfmt="{bz@@|@johndoe}`:: + Same as previous, but also show values added by 'johndoe' only. + +`$ git mr-show -c --cfmt="{bz@@|@johndoe,max,hugo}`:: + Same as previous limiting the values for more users. + +`$ git mr-show -c --cfmt="{bz@c}`:: + Show number of bz tag values. + +`$ git mr-show -c --cfmt="{bz@r@, }`:: + Same as previous, but show users who set the bz values and + separate them with ', '. + +`$ git mr-show -A -s --mfmt="{M} {bz@r@,}"`:: + Squash all tags for commits to MR and show MR hash along with bz + tag values. Values are displayed with users who set them and separated + by comma. + +`$ git mr-show -A -s --mfmt="{M} {bz@r@,:^20.20}"`:: + Same as previous, but also uses Python's format spec to set + maximum column width and align the bz values in center. + + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] + + +RESOURCES +--------- +Python's format string syntax: diff --git a/Documentation/git-mr-tag.txt b/Documentation/git-mr-tag.txt new file mode 100644 index 0000000..5192e5a --- /dev/null +++ b/Documentation/git-mr-tag.txt @@ -0,0 +1,99 @@ +GIT-MR-TAG(1) +============= +:doctype: manpage + + +NAME +---- +git-mr-tag - Add/remove tag for merge request or commits + + +SYNOPSIS +-------- +*git-mr-tag* ['OPTIONS'] ['REV' ['REV' ...]] + + +DESCRIPTION +----------- +Allows to attach tag to MR or commits. Each commit is identified by its +'REV' (hash). MR is a special case with zero 'REV' (0*40) and if no +commits are specified, it's used by default. This means you can attach tag +to each commit, but also to the MR as a whole. Tag name can consist of +alphanumeric chars only. Convention is to use uppercase letters for MR tags, +because commit tags can be squashed into the MR commit in 'git-mr-show' if the +*-s* option is used. This helps to avoid mixing of MR and commit tags. +If MR is not specified with the *-m* option, 'HEAD' is used by default. +Note that MR has to be specified along with the commits and commits have to be +part of the MR. + +User tags have reference under MR and user namespace +'refs/mrs///tags' and tags are stored separately for each user. +Any change to tags creates a new internal commit and proper ref is updated +accordingly. The tip of tags ref always contains the full set of user's tags. +The raw format can be displayed with +'git show refs/mrs///tags:tags'. + +Each tag can have multiple values set by different users, so each tag value +is tight with user who set it. This allows 'git-mr-show' to display tag +values for selected users only. Also different users can use the same tag name +and assign it different values. Tag is uniquely identified with tag name, value +and user who set this value. + +New tag value can be added with the *-a* option. If tag already exists, it's not +added. Only unique tags are stored. Removing tag with the *-r* option has to +modes. If value is specified with *-v*, only tag with this particular value +is removed. Without *-v* all tag values are removed. + + +OPTIONS +------- +*-A, --all*:: + Add tag to all commit(s) in MR. Commit(s) entered as positional arguments + or on *-* stdin are ignored. + +*-, --stdin*:: + Read commit(s) from stdin. Commit(s) entered as positional arguments are ignored. + +*-m, --mr 'REV'*:: + Merge request containing commit(s) (default: HEAD). + +*-a, --add*:: + Add tag. + +*-r, --remove*:: + Remove tag. If *-v* is not used remove all values for tag specified by *-t*. + +*-t, --tag* 'TAG':: + Specify 'TAG' name. It can contain alphanumeric chars only. + +*-v, --value* 'VALUE':: + Specify tag 'VALUE'. If used with *-r* only tags with this value + are removed. + + +EXAMPLES +-------- +`$ git mr-tag -a -t NOTE -v "This is my MR note"`:: + Add tag name NOTE with specified value to the MR identified by HEAD. + +`$ git mr-tag -A -a -t bz -v 12345`:: + Add tag name bz to all commits in MR identified by HEAD. + +`$ git mr-tag -A -a -t bz -v 12345 -m feature`:: + Same as previous, but MR is identified by the tip of feature branch. + Any git's rev expression can be used as long as it's valid MR. Meaning + MR is created for such REV. + +`$ git mr-tag -A -r -t bz -v 12345`:: + Remove value 12345 from tag bz for all commits in MR identified by HEAD. + +`$ git mr-tag -A -r -t bz`:: + Remove all values from tag bz for all commits in MR identified by HEAD. + +`$ git mr-tag -a -t bz -v 12345 -m feature ac13f5a23158286d`:: + Remove value 12345 from tag bz from commit ac13f5a23158286d in MR identified + by tip of feature branch. + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr-update.txt b/Documentation/git-mr-update.txt new file mode 100644 index 0000000..b34c525 --- /dev/null +++ b/Documentation/git-mr-update.txt @@ -0,0 +1,35 @@ +GIT-MR-UPDATE(1) +================ +:doctype: manpage + + +NAME +---- +git-mr-update - default git 'update' hook + + +SYNOPSIS +-------- +*git-mr-update* + + +DESCRIPTION +----------- +This is not intended to be run directly from command line. It's the default +push policy checker for 'gimmr'. Just create a symlink 'update' pointing to +'git-mr-update' in git's 'hooks' directory on your server. + +DEFAULT PUSH POLICY +------------------- +* Users listed in 'git config mr.admins' are not restricted +* Fast-forward ref updates only +* MR refs('head') can be created only, no updates are allowed +* Only refs in user's namespace can be updated('tags', 'mbox') +* Refs cannot be deleted +* Updates to 'tags' and 'mbox' refs are checked to be in expected format +* Only admins and user who created the MR are allowed to modify global + MR tags + +SEE ALSO +-------- +link:git-mr.html[git-mr(1)] diff --git a/Documentation/git-mr.txt b/Documentation/git-mr.txt new file mode 100644 index 0000000..f604776 --- /dev/null +++ b/Documentation/git-mr.txt @@ -0,0 +1,82 @@ +GIT-MR(1) +========= +:doctype: manpage + + +NAME +---- +git-mr - Set of simple tools to manage merge requests and their review within git + + +SYNOPSIS +-------- +*git-mr* + + +DESCRIPTION +----------- +'GIMRR' is abbreviation for **G**it **I**ntegrated **M**erge **R**equest **R**eview. +It's goal is to provide a simple, but effective approach how to manage and review +MRs in git. All data related to MRs are stored within git and 'git-mr' is just a +set of Python scripts to make this data available to users in some reasonable form. +This allows 'git-mr' to leverage git's transport protocol, distributed approach and +possibility to work offline. + +Once a MR is created by link:git-mr-create.html[git-mr-create(1)], you can +attach arbitrary tags to it or it's commits with link:git-mr-tag.html[git-mr-tag(1)] +and present them in a customizable format with +link:git-mr-show.html[git-mr-show(1)]. Review of MR can be done within your +favorite MUA, because 'git-mr' can generate mboxes with +link:git-mr-mbox.html[git-mr-mbox(1)] and link:git-mr-sendmail.html[git-mr-sendmail(1)] +can be used for mail delivery, which in context of 'git-mr' is creating a new +user's mbox entry. This should provide environment similar to email based +distribution of patches and their review. Since 'git-mr' just presents data +stored in git, other approaches and review methods can be implemented. + +All data are tracked within 'refs/mrs' refs namespace, where MRs are stored. MR is +identified by full SHA-1, which is a tip of a branch from which the MR was created. +This SHA-1 is also an unique identifier of the MR, which has several advantages. +You can pass MR to 'git-checkout' and use it as normal branch. Once on that branch +you don't need to specify MR for 'git-mr' commands, because by default they use +HEAD as a MR identifier. Each user has it's own refs namespace for each MR under +specified username. This allows to enforce a push policy easily, because each user +is allowed to push only his namespace. + +The full 'git-mr' ref format is 'refs/mrs///{head,tags,mbox}', where +'' is MR's SHA-1 identifier, is username as registered on server +and '{head,tags,mbox}' are three ref types used by 'git-mr', which are described +in *REF TYPES* section. + +REF TYPES +--------- +This is the core of 'git-mr' and all data are tracked using the following three +refs. + +head +~~~~ +This is just a ref to the commit from which the MR was created. + +tags +~~~~ +Ref to commit with user's current set of tags for a specific MR. It's +a git tree with a single 'tags' files in a specific format. It can be +viewed by using e.g. 'git show refs/mrs///tags:tags'. + +mbox +~~~~ +Ref to user's comments for a specific MR. It's an empty git tree and each +commit represents user's reply, which is stored as commit description. +These refs are used to generate MR mboxes and the format +can be viewed by using e.g. 'git log refs/mrs///mbox'. + +SEE ALSO +-------- +link:git-mr-show.html[git-mr-show(1)] +link:git-mr-create.html[git-mr-create(1)] +link:git-mr-tag.html[git-mr-tag(1)] +link:git-mr-fetch.html[git-mr-fetch(1)] +link:git-mr-push.html[git-mr-push(1)] +link:git-mr-remove.html[git-mr-remove(1)] +link:git-mr-sendmail.html[git-mr-sendmail(1)] +link:git-mr-update.html[git-mr-update(1)] +link:git-mr-mbox.html[git-mr-mbox(1)] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0474287 --- /dev/null +++ b/Makefile @@ -0,0 +1,84 @@ +.PHONY: clean rpm doc man html install uninstall clean_man clean_html + +all:: + +RM = rm -f +INSTALL = install +RPMBUILD = rpmbuild + +DESTDIR = +DOCDIR = Documentation +MANDIR = $(DOCDIR)/man +HTMLDIR = $(DOCDIR)/html + +prefix = /usr/local +bindir = $(prefix)/bin +mandir = $(prefix)/share/man +man1dir = $(mandir)/man1 + +BIN_FILES = git-mr-create \ + git-mr-fetch \ + git-mr-mbox \ + git-mr-push \ + git-mr-remove \ + git-mr-sendmail \ + git-mr-show \ + git-mr-tag \ + git-mr-update \ + mrlib.py + +MAN1_TXT_FILES = $(DOCDIR)/git-mr-show.txt \ + $(DOCDIR)/git-mr-create.txt \ + $(DOCDIR)/git-mr-tag.txt \ + $(DOCDIR)/git-mr-fetch.txt \ + $(DOCDIR)/git-mr-push.txt \ + $(DOCDIR)/git-mr-remove.txt \ + $(DOCDIR)/git-mr-sendmail.txt \ + $(DOCDIR)/git-mr-update.txt \ + $(DOCDIR)/git-mr-mbox.txt \ + $(DOCDIR)/git-mr.txt + +MAN1_FILES = $(patsubst $(DOCDIR)/%.txt, $(MANDIR)/%.1, $(MAN1_TXT_FILES)) +HTML_FILES = $(patsubst $(DOCDIR)/%.txt, $(HTMLDIR)/%.html, $(MAN1_TXT_FILES)) + +$(MANDIR)/%.1: $(DOCDIR)/%.txt + a2x --destination-dir=$(MANDIR) --doctype manpage --format manpage $< + +$(HTMLDIR)/%.html: $(DOCDIR)/%.txt + a2x --destination-dir=$(HTMLDIR) --doctype manpage --format xhtml $< + +$(MANDIR) $(HTMLDIR): + mkdir -p $@ + +$(MAN1_FILES): | $(MANDIR) +$(HTML_FILES): | $(HTMLDIR) + +man: $(MAN1_FILES) + +html: $(HTML_FILES) + +clean_man: + $(RM) -r $(MANDIR) + +clean_html: + $(RM) -r $(HTMLDIR) + +RPM_TOP_DIR = $(CURDIR)/rpmbuild +rpm: + mkdir -p $(RPM_TOP_DIR)/{BUILD,RPMS,SPECS,SRPMS} + $(RPMBUILD) -bb --build-in-place --define "_topdir $(RPM_TOP_DIR)" gimrr.spec + +doc: man html + +clean: clean_man clean_html + $(RM) -r $(RPM_TOP_DIR) + +install: $(BIN_FILES) $(MAN1_FILES) + $(INSTALL) -d -m 755 $(DESTDIR)$(bindir) + $(INSTALL) -m 755 $(BIN_FILES) $(DESTDIR)$(bindir) + $(INSTALL) -d -m 755 $(DESTDIR)$(man1dir) + $(INSTALL) -m 644 $(MAN1_FILES) $(DESTDIR)$(man1dir) + +uninstall: + $(RM) $(patsubst %, $(DESTDIR)$(bindir)/%, $(notdir $(BIN_FILES))) + $(RM) $(patsubst %, $(DESTDIR)$(man1dir)/%, $(notdir $(MAN1_FILES))) diff --git a/gimrr.spec b/gimrr.spec new file mode 100644 index 0000000..5a2c596 --- /dev/null +++ b/gimrr.spec @@ -0,0 +1,28 @@ +Name: gimrr +Version: 0 +Release: 1 +Summary: Set of simple tools to manage merge requests and their review within git +License: WTFPL +BuildArch: noarch +BuildRequires: asciidoc +Requires: python3 >= 3.2 +Requires: git + +%description +Dumb, buggy, incomplete and especially NOT modern tool for old farts without WebUI. + +%prep + +%build +make man + +%install +make install DESTDIR=%{buildroot} bindir=%{_bindir} mandir=%{_mandir} + +%files +%defattr(-,root,root) +%{_bindir}/* +%{_mandir}/man1/* +%license COPYING + +%changelog diff --git a/git-mr-create b/git-mr-create new file mode 100755 index 0000000..4f23434 --- /dev/null +++ b/git-mr-create @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import argparse +import tempfile +import sys +import mrlib +import time + +init_desc=""" + +# Please enter the MR commit message for your changes. Lines starting +# with '#' will be ignored, and an empty message aborts the commit. +""" + +parser = argparse.ArgumentParser(description="Create new merge request."); +parser.add_argument("mr", nargs="?", default="HEAD", metavar="REV", + help="New merge request revision (default: HEAD)") +parser.add_argument("-", "--stdin", action="store_true", + help="Read MR description from stdin. Description entered via -m or -F option is ignored.") +parser.add_argument("-F", "--file", metavar="FILE", + help="Use MR description from FILE. Description entered via -m option is ignored.") +parser.add_argument("-m", "--message", nargs="?", metavar="MSG", action="append", + help="Use MSG as MR description. Multiple MSGs are concatenated as separate paragraphs.") +parser.add_argument("-b", "--base", metavar="REV", required=True, + help="Base revision of the merge request.") + +args = parser.parse_args() + +mr = mrlib.verify_rev(args.mr) +base = mrlib.verify_rev(args.base) + +if mr == base: + sys.exit("No new commits.") + +user = mrlib.get_user() +tmpdir = tempfile.TemporaryDirectory() + +tags = dict() +mrlib.add_tag(tags, mr, mrlib.rev_zero, "B", user, base) + +if args.stdin: + desc = sys.stdin.read() +elif args.file: + with open(args.file, "r") as f: + desc = f.read() +elif args.message: + desc = "\n\n".join(args.message) +else: + desc = mrlib.editor(init_desc) + +desc = desc.strip() + +split = desc.split("\n\n", 1) +subj = split[0].replace("\n", " ") +subj = subj.strip() +if not subj: + sys.exit("Empty MR commit message. Aborting...") + +desc = "" +if len(split) > 1: + desc = "".join(split[1:]) + desc = desc.replace("\n", "\\n") + +an = mrlib.git_config("user.name") +if not an: + sys.exit("No git user.name found.") + +ae = mrlib.git_config("user.email") +if not ae: + sys.exit("No git user.email found.") +at = int(time.time()) + +mrlib.add_tag(tags, mr, mrlib.rev_zero, "AN", user, an) +mrlib.add_tag(tags, mr, mrlib.rev_zero, "AE", user, ae) +mrlib.add_tag(tags, mr, mrlib.rev_zero, "AT", user, at) + +mrlib.add_tag(tags, mr, mrlib.rev_zero, "S", user, subj) +if desc: + mrlib.add_tag(tags, mr, mrlib.rev_zero, "D", user, desc) + +fmt = "--format=format:%H s %s%n%H an %an%n%H ae %ae%n%H at %at" +rv, out, err = mrlib.run(["git", "log", fmt, "{}..{}".format(base, mr)]) + +commits = list() +for line in out.split("\n"): + commit,tag,value= line.split(None, 2) + + if commit not in commits: + commits.insert(0, commit) + + mrlib.add_tag(tags, mr, commit, tag, user, value) + +mrlib.add_tag(tags, mr, mrlib.rev_zero, "CC", user, str(len(commits))) +mrlib.add_tag(tags, mr, mrlib.rev_zero, "C", user, " ".join(commits)) + +tagsfn = "{}/tags".format(tmpdir.name) +with open(tagsfn, 'w+') as f: + mrlib.store_user_tags(tags, mr, user, f) + +tagssha = mrlib.commit_file(tagsfn, "tags", "Initial tags for MR {}".format(mr)) + +mrlib.update_ref("refs/mrs/{}/{}/head".format(mr, user), mr, oldrev=mrlib.rev_zero) +mrlib.update_ref("refs/mrs/{}/{}/tags".format(mr, user), tagssha, oldrev=mrlib.rev_zero) diff --git a/git-mr-fetch b/git-mr-fetch new file mode 100755 index 0000000..c5b13ee --- /dev/null +++ b/git-mr-fetch @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import argparse +import sys +import mrlib + +parser = argparse.ArgumentParser(description="Download merge request(s), tags and mboxes from remote."); +parser.add_argument("mr", nargs="*", default=["HEAD"], metavar="MR", + help="MR revision (default: HEAD).") +parser.add_argument("-A", "--all", action='store_true', + help="Fetch all refs from remote. MRs entered as positional arguments or on stdin are ignored.") +parser.add_argument("-", "--stdin", action='store_true', + help="Read MRs from stdin. MRs entered as positional arguments are ignored.") +parser.add_argument("-r", "--remote", metavar="REPOSITORY", default="origin", + help="The remote repository to fetch from (default: origin).") +parser.add_argument("--head", action='store_true', + help="Fetch MR commits.") +parser.add_argument("--tags", action='store_true', + help="Fetch tags.") +parser.add_argument("--mbox", action='store_true', + help="Fetch mboxes.") +parser.add_argument("-f", "--force", action='store_true', + help="Fetch also refs for current user. All local changes will be lost.") +parser.add_argument("-q", "--quiet", action='store_true', + help="Progress is not reported to the standard error stream.") + +args = parser.parse_args() +user = mrlib.get_user() +currefs = mrlib.load_refs() +types = [] + +if args.all: + mrs = [] +elif args.stdin: + mrs = mrlib.verify_mrs(sys.stdin.readlines(), currefs) +else: + mrs = mrlib.verify_mrs(args.mr, currefs) + +# In future switch to negative refs introduced in git 2.28 +rv, out, err = mrlib.run(["git", "ls-remote", args.remote, "refs/mrs/*"]) + +remote_refs = mrlib.load_refs(buf=out) +newrefs = mrlib.new_refs(currefs, remote_refs) +if not newrefs: + sys.exit() + +if args.head: + types += ["head"] + +if args.tags: + types += ["tags"] + +if args.mbox: + types += ["mbox"] + +if not types: + types = ["head", "mbox", "tags"] + +if args.force: + newrefs = mrlib.filter_refs(newrefs, mrs=mrs, types=types) +else: + newrefs = mrlib.filter_refs(newrefs, mrs=mrs, not_users=[user], types=types) + +newrefs = ["{}:{}".format(ref, ref) for rev, ref in newrefs] + +if not newrefs: + sys.exit() + +cmd = ["git", "fetch"] +if args.quiet: + cmd += ["--quiet"] + +cmd += [args.remote] + newrefs + +rv, out, err = mrlib.run(cmd) +if err: + print(err, end="") diff --git a/git-mr-mbox b/git-mr-mbox new file mode 100755 index 0000000..8feb15d --- /dev/null +++ b/git-mr-mbox @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import subprocess +import argparse +import sys +import mrlib +import fcntl +import os +import tempfile + +parser = argparse.ArgumentParser(description="Generate mbox for merge request(s) and print it to stdout."); +parser.add_argument("mr", nargs="*", default=["HEAD"], metavar="MR", + help="MR revision (default: HEAD)") +parser.add_argument("-A", "--all", action='store_true', + help="Use all available MRs as input. MRs entered as positional arguments or on stdin are ignored.") +parser.add_argument("-", "--stdin", action='store_true', + help="Read MRs from stdin. MRs entered as positional arguments are ignored.") +parser.add_argument("-r", "--read-refs", metavar="FILE", + help="Read previously stored refs from FILE and use them as a start point for mbox generation. If not specified, complete MRs mbox will be generated.") +parser.add_argument("-w", "--write-refs", metavar="FILE", + help="Write updated refs to FILE, which may be later used as input for the -r option. Using both -r and -w option with the same FILE, along with the -m option, will incrementally update mbox with new messages only.") +parser.add_argument("-m", "--mbox", metavar="FILE", + help="Append generated mbox to FILE instead of printing it to stdout. If FILE doesn't exist, it's created.") + +args = parser.parse_args() + +currefs = mrlib.load_refs() +currefs = mrlib.filter_refs(currefs) +newrefs = [] +oldrefs = [] + +if args.read_refs and os.path.exists(args.read_refs): + rfd = open(args.read_refs, "r") + fcntl.lockf(rfd.fileno(), fcntl.LOCK_SH | fcntl.LOCK_NB) + oldrefs = mrlib.load_refs(fd=rfd) + +if not mrlib.new_refs(oldrefs, currefs, types=["head", "mbox"]): + sys.exit() + +if args.write_refs: + wfd_old = open(args.write_refs, "a+") + fcntl.lockf(wfd_old.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + wfd_old.seek(0) + +if args.mbox: + mfd = open(args.mbox, "ab+") + fcntl.lockf(mfd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + +if args.all: + mrs = mrlib.filter_revs(currefs, types=["head"]) +elif args.stdin: + mrs = mrlib.verify_mrs(sys.stdin.readlines(), currefs) +else: + mrs = mrlib.verify_mrs(args.mr, currefs) + +if args.write_refs: + wdir = tempfile.TemporaryDirectory(dir=os.path.dirname(os.path.realpath(args.write_refs))) + wfn_new = "{}/write_refs".format(wdir.name) + wfd_new = open(wfn_new, "w") + +if args.mbox: + try: + pos = mfd.tell() + mrlib.store_mbox(mrs, oldrefs, currefs, newrefs, mfd) + mfd.flush() + except: + mfd.truncate(pos) + raise +else: + mrlib.store_mbox(mrs, oldrefs, currefs, newrefs, sys.stdout.buffer) + +if args.write_refs: + mrlib.store_refs(newrefs, wfd_new) + wfd_new.close() + os.rename(wfn_new, args.write_refs) diff --git a/git-mr-push b/git-mr-push new file mode 100755 index 0000000..f2b8e18 --- /dev/null +++ b/git-mr-push @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import argparse +import sys +import mrlib + +parser = argparse.ArgumentParser(description="Update remote merge request(s), tags and mboxes."); +parser.add_argument("mr", nargs="*", default=["HEAD"], metavar="MR", + help="MR revision (default: HEAD)") +parser.add_argument("-A", "--all", action='store_true', + help="Use all available MRs as input. MRs entered as positional arguments or on stdin are ignored.") +parser.add_argument("-", "--stdin", action='store_true', + help="Read MRs from stdin. MRs entered as positional arguments are ignored.") +parser.add_argument("-r", "--remote", metavar="REPOSITORY", default="origin", + help="The remote repository that is destination of a push operation (default: origin).") +parser.add_argument("--head", action='store_true', + help="Push MR commits.") +parser.add_argument("--tags", action='store_true', + help="Push tags.") +parser.add_argument("--mbox", action='store_true', + help="Push mboxes.") +parser.add_argument("-q", "--quiet", action='store_true', + help="Progress is not reported to the standard error stream.") + +args = parser.parse_args() +user = mrlib.get_user() +refs = mrlib.load_refs() +push_refs = [] +types = [] + +if args.head: + types += ["head"] + +if args.tags: + types += ["tags"] + +if args.mbox: + types += ["mbox"] + +if not types: + types = ["head", "mbox", "tags"] + +if args.all: + mrs = mrlib.filter_revs(refs, types=["head"], users=[user]) +else: + if args.stdin: + mrs = mrlib.verify_mrs(sys.stdin.readlines(), refs) + else: + mrs = mrlib.verify_mrs(args.mr, refs) + +# push the heads first +for type in types: + push_refs += mrlib.filter_refs(refs, mrs, users=[user], types=[type]) + +push_refs = ["{}:{}".format(ref, ref) for rev, ref in push_refs] + +if not push_refs: + sys.exit() + +cmd = ["git", "push"] +if args.quiet: + cmd += ["--quiet"] + +cmd += [args.remote] + push_refs + +rv, out, err = mrlib.run(cmd) +if err: + print(err, end="") diff --git a/git-mr-remove b/git-mr-remove new file mode 100755 index 0000000..abd8cd1 --- /dev/null +++ b/git-mr-remove @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import argparse +import mrlib +import sys + +parser = argparse.ArgumentParser(description="Remove merge request(s), tags and mboxes"); +parser.add_argument("mr", nargs="*", default=["HEAD"], metavar="MR", + help="Merge request revisions (default: HEAD)") +parser.add_argument("-A", "--all", action="store_true", + help="Use all available MRs as input. MRs entered as positional arguments or on stdin are ignored.") +parser.add_argument("-", "--stdin", action="store_true", + help="Read MRs from stdin. MRs entered as positional arguments are ignored.") +parser.add_argument("--head", action="store_true", + help="Remove MR commits.") +parser.add_argument("--tags", action="store_true", + help="Remove tags.") +parser.add_argument("--mbox", action="store_true", + help="Remove mboxes.") +parser.add_argument("-u", "--user", nargs="?", metavar="USER", action="append", + help="Remove refs for specific user only. This option can be used multiple times.") + +args = parser.parse_args() +user = mrlib.get_user() +refs = mrlib.load_refs() +types = list() + +if args.head: + types += ["head"] + +if args.tags: + types += ["tags"] + +if args.mbox: + types += ["mbox"] + +if not types: + types = ["head", "mbox", "tags"] + +if args.user and args.user[0] == None: + args.user = [user] + +if args.all: + mrs = mrlib.filter_revs(refs, types=["head"]) +elif args.stdin: + mrs = mrlib.verify_mrs(sys.stdin.readlines(), refs) +else: + mrs = mrlib.verify_mrs(args.mr, refs) + +refs = mrlib.filter_refs(refs, mrs=mrs, types=types, users=args.user) + +if not refs: + sys.exit() + +for rev, ref in refs: + mrlib.update_ref(ref, delete=True) diff --git a/git-mr-sendmail b/git-mr-sendmail new file mode 100755 index 0000000..e60fa18 --- /dev/null +++ b/git-mr-sendmail @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import subprocess +import argparse +import tempfile +import sys +import mrlib +import re + +def get_hdr_value(hdrs, name): + for hdr in hdrs: + h = hdr.split(':', 1) + if len(h) != 2: + continue + n, v = h + if n.lower() == name.lower(): + return v.strip() + return "" + +tmpdir = tempfile.TemporaryDirectory() + +parser = argparse.ArgumentParser(description="Simulate sendmail behafiour to send reply to MR, commit or comment."); +parser.add_argument("recipient", nargs="*", metavar="RECIPIENT", + help="Positional argumets are ignored. This is for sendmail compatibility only.") +parser.add_argument("-f", "--file", metavar="FILE", + help="Read reply fro FILE instead from stdin.") + +args=parser.parse_args() + +user = mrlib.get_user() + +hdrs = [] +body = "" + +if args.file: + fd = open(args.file, "r") +else: + fd = sys.stdin + +mail = fd.read().split("\n\n", 1) +if len(mail) > 1: + body = mail[1] + +for i, hdr in enumerate(mail[0].split("\n")): + if hdr.startswith((' ', '\t')): + hdrs[-1] += " " + hdr.lstrip() + else: + hdrs.append(hdr) + +inreplyto = get_hdr_value(hdrs, "in-reply-to") +if not inreplyto: + sys.exit("In-Reply-To header not found.") + +match = re.match("<([0-9a-f]{40})", inreplyto) +if not match: + sys.exit("Cannot find MR in In-Reply-To header.") + +mr = match[1] +refs = mrlib.load_refs() +mr = mrlib.verify_mrs([mr], refs)[0] + +subj = get_hdr_value(hdrs, "subject") +if not subj: + sys.exit("Subject header not found.") + +to = get_hdr_value(hdrs, "to") +if not subj: + sys.exit("To header not found.") + +mboxfn = "{}/mbox".format(tmpdir.name) + +with open(mboxfn, mode="w") as f: + f.write("{}\n\nTo: {}\nIn-Reply-To: {}\n\n".format(subj, to, inreplyto)) + f.write(body) + +with open(mboxfn, mode="r") as f2: + print(f2.read()) + +oldref = mrlib.filter_revs(refs, mrs=[mr], users=[user], types=["mbox"]) + +if oldref: + oldrev = oldref[0] +else: + oldrev = None + +mboxsha = mrlib.commit_file(None, None, None, oldrev, mboxfn) + +if not oldrev: + oldrev = mrlib.rev_zero + +mrlib.update_ref("refs/mrs/{}/{}/mbox".format(mr, user), mboxsha, oldrev=oldrev) diff --git a/git-mr-show b/git-mr-show new file mode 100755 index 0000000..7318354 --- /dev/null +++ b/git-mr-show @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import argparse +import string +import sys +import subprocess +import mrlib + +mfmt = "{M:.16} {B:.16} {CC:6.6} {S:60.60} {AN:16.16} {AE:>20.20} {AD}" +cfmt = " {h:.16} {s}" +parser = argparse.ArgumentParser(description="Show information about merge request(s) and its commits.") +parser.add_argument("mr", nargs="*", default=["HEAD"], metavar="REV", + help="MR revision(s) for which to show information (default: HEAD).") +parser.add_argument("-m", "--my", action="store_true", + help="Show information for MRs created by me. MRs entered as positional arguments, on stdin or with --all option are ignored.") +parser.add_argument("-A", "--all", action="store_true", + help="Show information for all MRs. MRs entered as positional arguments or on stdin are ignored.") +parser.add_argument("-", "--stdin", action="store_true", + help="Read MRs from stdin. MRs entered as positional arguments are ignored.") +parser.add_argument("-c", "--commits", action="store_true", + help="Show also commits information for each MR.") +parser.add_argument("-s", "--squash", action="store_true", + help="Squash commit tags into MR.") +parser.add_argument("--mfmt", metavar="FMT", default=mfmt, + help="Python's formatted string for MRs.") +parser.add_argument("--cfmt", metavar="FMT", default=cfmt, + help="Python's formatted string for MR comments.") +parser.add_argument("-t", "--tags", action="store_true", + help="List available tags for selected MRs.") +parser.add_argument("-u", "--user", nargs="?", metavar="USER", action="append", + help="Show tags for specified user(s) only. Can be used multiple times.") + +args = parser.parse_args() +user = mrlib.get_user() + +class TagFormatter(string.Formatter): + def __init__(self): + pass + + def get_field(self, field, a, ctags): + f = field.split("@", 3) + c = len(f) + + name = f[0] + fmt = "v" + users = None + sep = " " + + if c == 4: + fmt = f[1] + sep = f[2] + users = f[3] + elif c == 3: + fmt = f[1] + sep = f[2] + elif c == 2: + fmt = f[1] + + if not fmt: + fmt = "v" + if not sep: + sep = " " + if users: + users = users.split(",") + elif args.user: + if args.user[0] == None: + users = [user] + else: + users = args.user + + tag = mrlib.get_tag(ctags, name) + if not tag: + return ("{{{}}}".format(field), None) + + value = None + + if fmt == "v": + values = mrlib.get_tag_values(ctags, name, users) + if values: + value = sep.join(values) + + elif fmt == "c": + values = mrlib.get_tag_values(ctags, name, users) + if values: + value = str(len(values)) + + elif fmt == "r": + value = sep.join("{}:{}".format(u, v) for u, v in tag) + + else: + sys.exit("Unknown format.") + + if not value: + value = "{{{}}}".format(field) + + return (value, field) + +tfmt = TagFormatter() +def printt(fmt, ctags): + print(tfmt.vformat(fmt, None, ctags)) + +refs = mrlib.load_refs() + +if args.my: + mrs = mrlib.filter_revs(refs, types=["head"], users=[user]) +elif args.all: + mrs = mrlib.filter_revs(refs, types=["head"]) +else: + if args.stdin: + mrs = sys.stdin.readlines() + else: + mrs = args.mr + + mrs = mrlib.verify_mrs(mrs, refs) + +tags = mrlib.load_tags(mrs, refs=refs, commits=args.commits, squash=args.squash) + +mrs_sorted = list() +for mr in mrs: + ctags = mrlib.get_commit_tags(tags, mr, mrlib.rev_zero) + if not ctags: + continue + + dt = mrlib.get_tag_value(ctags, "AT") + mrs_sorted += [(mr,dt)] + +mrs_sorted = sorted(mrs_sorted, key=lambda mr: mr[1]) + +tag_desc = { + "M": "MR sha", + "A": "MR author", + "S": "MR or commit subject", + "D": "MR description", + "B": "MR base sha", + "C": "MR commit list", + "CC": "MR commit count", + "AN": "MR author name", + "AE": "MR author email", + "AT": "MR author date, UNIX timestamp", + "AI": "MR author date, ISO 8601-like format", + "AD": "MR author date", + "h": "commit sha", + "an": "author name", + "ae": "author email", + "at": "author date, UNIX timestamp", + "ai": "author date, ISO 8601-like format", + "ad": "author date", + } + +if args.tags: + tset = {"M", "h"} + for commits in tags.values(): + for tags in commits.values(): + for tag in tags: + tset.add(tag) + for t in tset: + if t in tag_desc: + print("{:10.10} {}".format(t, tag_desc[t])) + else: + print("{:10.10} description not available".format(t)) + sys.exit() + +for mr, dt in mrs_sorted: + mrtags = mrlib.get_commit_tags(tags, mr, mrlib.rev_zero) + if not mrtags: + continue + + mrtags["M"] = {("", mr)} + + printt(args.mfmt, mrtags) + + if args.commits: + commits = mrlib.get_tag_value(mrtags, "C") + if not commits: + continue + + for commit in commits.split(): + ctags = mrlib.get_commit_tags(tags, mr, commit) + if not ctags: + continue + ctags["M"] = {("", mr)} + ctags["h"] = {("", commit)} + + printt(args.cfmt, ctags) diff --git a/git-mr-tag b/git-mr-tag new file mode 100755 index 0000000..41d44d6 --- /dev/null +++ b/git-mr-tag @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import subprocess +import argparse +import tempfile +import sys +import mrlib + +parser = argparse.ArgumentParser(description="Add/remove tag for merge request or commits."); +parser.add_argument("commit", nargs="*", default=[mrlib.rev_zero], metavar="COMMIT", + help="Commit(s) to which the tag will be added or moreved. (default: MR commit)") +parser.add_argument("-", "--stdin", action="store_true", + help="Read commit(s) from stdin. Commit(s) entered as positional arguments are ignored.") +parser.add_argument("-m", "--mr", metavar="MR", default="HEAD", + help="Merge request containing commit(s) (default: HEAD).") +parser.add_argument("-a", "--add", action="store_true", + help="Add tag.") +parser.add_argument("-r", "--remove", action="store_true", + help="Remove tag.") +parser.add_argument("-A", "--all", action="store_true", + help="Add tag to all commit(s) in merge request. Commit(s) entered as positional arguments or on stdin are ignored.") +parser.add_argument("-t", "--tag", required=True, metavar="TAG", + help="Tag name.") +parser.add_argument("-v", "--value", metavar="VALUE", + help="Tag value.") + +args = parser.parse_args() + +if not args.add and not args.remove: + sys.exit("Either add or remove operation has to be specified.") + +if args.add and not args.value: + sys.exit("Adding tag requires value.") + +value = None +if args.value: + value = args.value.strip() + if not value: + sys.exit("Empty value.") + +tag = args.tag.strip() +if not tag.isalnum(): + sys.exit("Invalid tag name.") + +user = mrlib.get_user() +refs = mrlib.load_refs() +mr = mrlib.verify_mrs([args.mr], refs)[0] +tags = mrlib.load_tags([mr], refs=refs, commits=False) + +mrtags = mrlib.get_commit_tags(tags, mr, mrlib.rev_zero) +if not mrtags: + sys.exit("No MR tags found.") + +mrcommits = mrlib.get_tag_value(mrtags, "C") +if not mrcommits: + sys.exit("No MR commits found.") + +mrcommits = mrcommits.split() + +if args.all: + commits = mrcommits +elif args.stdin: + commits = mrlib.verify_commits(mrcommits, sys.stdin.readlines()) +else: + commits = mrlib.verify_commits(mrcommits, args.commit) + +tags = mrlib.load_tags([mr], refs=refs, users=[user]) + +if args.add: + for commit in commits: + mrlib.add_tag(tags, mr, commit, tag, user, value) + +elif args.remove: + for commit in commits: + mrlib.remove_tag(tags, mr, commit, tag, user, value) + +tmpdir = tempfile.TemporaryDirectory() +tagsfn = "{}/tags".format(tmpdir.name) +mr = mrlib.verify_rev(args.mr) + +with open(tagsfn, 'w+') as f: + mrlib.store_user_tags(tags, mr, user, f) + +oldrev = mrlib.filter_revs(refs, mrs=[mr], users=[user], types=["tags"]) +if not oldrev: + oldrev = mrlib.rev_zero + parent = None +else: + oldrev = oldrev[0] + parent = oldrev + +tagssha = mrlib.commit_file(tagsfn, "tags", "Updated tags for MR {}".format(mr), parent) + +mrlib.update_ref("refs/mrs/{}/{}/tags".format(mr, user), tagssha, oldrev=oldrev) diff --git a/git-mr-update b/git-mr-update new file mode 100755 index 0000000..f2a0971 --- /dev/null +++ b/git-mr-update @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import mrlib +import sys +import os +import re + +argc = len(sys.argv) + +if argc != 4: + sys.exit("usage: {} ".format(sys.argv[0])) + +refname = sys.argv[1] +oldrev = sys.argv[2] +newrev = sys.argv[3] + +admins = mrlib.git_config("mr.admins") +if not admins: + sys.exit("No git-mr admins found.") + +admins = admins.split() + +user = os.environ.get("GIMRR_USER") +if not user: + sys.exit("No git-mr user found.") + +if user in admins: + sys.exit() + +if newrev == mrlib.rev_zero: + sys.exit("You cannot delete ref '{}'.".format(refname)) + +match = re.match("^refs/mrs/([0-9a-f]{40})/([^/]+)/(head|tags|mbox)$", refname) +if not match: + sys.exit("You cannot push ref '{}'.".format(refname)) + +ref_mr = match.group(1) +ref_user = match.group(2) +ref_type = match.group(3) + +if ref_user != user: + sys.exit("User '{}' in ref '{}' does not match user '{}' on server.".format(ref_user, refname, user)) + +if ref_type not in ["head", "tags", "mbox"]: + sys.exit("Invalid ref type '{}' in '{}'. Allowed types are 'head', 'tags' or 'mbox'.".format(ref_type, refname)) + +if oldrev != mrlib.rev_zero: + rv, out, err = mrlib.run(["git", "merge-base", "--is-ancestor", oldrev, newrev], die=False) + if rv: + sys.exit("Non-fast-forward update of ref '{}'.".format(refname)) + +if ref_type == "head": + if oldrev != mrlib.rev_zero: + sys.exit("You are not allowed to update MR head for ref '{}'.".format(refname)) + + if newrev != ref_mr: + sys.exit("MR head rev '{}' does not match MR '{}' in '{}'.".format(newrev, ref_mr, refname)) + + # This is racy, but better this than nothing. If two users, by any chance, + # push the same MR, it will be visible and can be dealt with. + rv, out, err = mrlib.run(["git", "for-each-ref", "refs/mrs/{}/*/head".format(ref_mr)]) + out = out.strip() + if out: + sys.exit("MR head '{}' already exists as '{}'.".format(ref_mr, out)) + +elif ref_type == "tags": + rv, out, err = mrlib.run(["git", "for-each-ref", "--format=%(refname)", + "refs/mrs/{}/*/head".format(ref_mr)]) + if not out: + sys.exit("No MR '{}' exists on server.".format(ref_mr)) + + match = re.match("^refs/mrs/{}/([^/]+)/head$".format(ref_mr), out) + mr_owner = match.group(1) + + rv, out, err = mrlib.run(["git", "show", "{}:tags".format(newrev)]) + tags = mrlib.load_user_tags(out) + + mr_tags = mrlib.get_commit_tags(tags, ref_mr, mrlib.rev_zero) + if not mr_tags: + sys.exit() + + if user != mr_owner: + sys.exit("MR tags can be set by owners and admins only.") + +elif ref_type == "mbox": + stdin = "{}\n".format(newrev) + if oldrev != mrlib.rev_zero: + stdin += "^{}\n".format(oldrev) + + revs = list() + rv, out, err = mrlib.run(["git", "rev-list", "--stdin"], stdin=stdin) + for line in out.split("\n"): + if not line: + continue + revs.append(line) + + for rev in revs: + rv, out, err = mrlib.run(["git", "show", "--format=%b", rev]) + lines = out.split("\n") + if len(lines) < 3: + sys.exit("Invalid comment format. Expecting at least three lines.") + + if not lines[0].lower().startswith("to: "): + sys.exit("Expecting 1st line '{}' to be 'To' header.".format(lines[0])) + + if not lines[1].lower().startswith("in-reply-to: "): + sys.exit("Expecting 2nd line '{}' to be 'In-Reply-To' header.".format(lines[1])) + + if lines[2]: + sys.exit("Expecting 3rd line '{}' to be empty.".format(lines[2])) + +sys.exit() diff --git a/mrlib.py b/mrlib.py new file mode 100755 index 0000000..8869874 --- /dev/null +++ b/mrlib.py @@ -0,0 +1,499 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import subprocess +import sys +import tempfile +import re +import fcntl +import datetime + +from_line = "From localhost.localdomain Wed Sep 1 08:00:00 CET 2010" +rev_zero = "0"*40 + +def run(cmd, stdin=None, stdout=True, stderr=True, text=True, env=None, die=True): + if stdin and text: + stdin = stdin.encode() + + p = subprocess.Popen(cmd, + stdin=subprocess.PIPE if stdin else None, + stdout=subprocess.PIPE if stdout else None, + stderr=subprocess.PIPE if stderr else None, + env=env) + + out, err = p.communicate(input=stdin) + if not stdout: + out = b"" + if not stderr: + err = b"" + + if text: + out = out.decode() + err = err.decode() + + if die and p.returncode: + if not text: + err = err.decode() + + sys.exit(err) + + return (p.returncode, out, err) + +def new_refs(oldrefs, currefs, types=None): + if types: + oldrefs = filter_refs(oldrefs, types=types) + currefs = filter_refs(currefs, types=types) + old = set(oldrefs) + cur = set(currefs) + new = cur - old + return list(new) + +def load_refs(fd=None, buf=None): + refs = list() + + if buf != None: + out = buf + elif fd != None: + out = fd.read() + else: + rv, out, err = run(["git", "for-each-ref", + "--format=%(objectname) %(refname)", "refs/mrs/*/*/*"]) + + out = out.rstrip() + if not out: + return [] + + for line in out.split("\n"): + rev, ref = line.split() + refs.append((rev, ref)) + + return refs + +def store_refs(refs, fd): + for rev, ref in refs: + fd.write("{} {}\n".format(rev, ref)) + +def filter_refs(refs_in, mrs=None, users=None, types=None, + not_mrs=None, not_users=None, not_types=None): + + refs = list() + for rev, ref in refs_in: + refs_dir, mrs_dir, mr, user, type = ref.split("/") + + if not_mrs and mr in not_mrs: + continue + + if not_users and user in not_users: + continue + + if not_types and type in not_types: + continue + + if mrs and mr not in mrs: + continue + + if users and user not in users: + continue + + if types and type not in types: + continue + + refs.append((rev, ref)) + + return refs + +def filter_revs(refs_in, mrs=None, users=None, types=None, + not_mrs=None, not_users=None, not_types=None): + refs = filter_refs(refs_in, mrs, users, types, + not_mrs, not_users, not_types) + return [rev for rev, ref in refs] + +def verify_rev(rev): + rev = rev.strip() + rv, out, err = run(["git", "rev-parse", "--verify", rev]) + if rv: + sys.exit("Invalid rev: {}".format(rev)) + + return out.rstrip() + +def verify_mrs(mrs, refs): + revs = set() + for mr in mrs: + rev = verify_rev(mr) + revs.add(rev) + + valid = set(filter_revs(refs, types=["head"])) + diff = revs - valid + if diff: + sys.exit("Invalid or non-existing MR: {}".format(" ".join(diff))) + + return list(revs) + +def verify_commits(mrcommits, commits): + mrc = set(mrcommits) + mrc.add(rev_zero) + c = set(commits) + diff = c - mrc + if diff: + sys.exit("Invalid commits: {}".format(" ".join(diff))) + + return commits + + +def add_tag(tags, mr, commit, tag, user, value): + if mr not in tags: + tags[mr] = dict() + + if commit not in tags[mr]: + tags[mr][commit] = dict() + + if tag not in tags[mr][commit]: + tags[mr][commit][tag] = set() + + tags[mr][commit][tag].add((user, value)) + +def remove_tag(tags, mr, commit, tag, user=None, value=None): + if mr not in tags: + return + + if not commit in tags[mr]: + return + + if not tag in tags[mr][commit]: + return + + if not user and not value: + del tags[mr][commit][tag] + return + + if user: + tags[mr][commit][tag] = {(u,v) for u,v in tags[mr][commit][tag] if u != user} + + if value: + tags[mr][commit][tag] = {(u,v) for u,v in tags[mr][commit][tag] if v != value} + + if not tags[mr][commit][tag]: + del tags[mr][commit][tag] + +def load_tags(mrs, refs=None, users=None, commits=True, squash=False): + tags = dict() + + if not refs: + refs = load_refs() + + revs = filter_revs(refs, mrs=mrs, users=users, types=["tags"]) + if not revs: + return tags + + stdin = "" + for rev in revs: + stdin += "{}:tags\n".format(rev) + + rv, out, err = run(["git", "cat-file", "--batch"], stdin=stdin) + + mr = None + user = None + commit = None + for line in out.split("\n"): + if not line: + continue + + tag, value = line.split(None, 1) + if tag == "@m": + mr = value + continue + + if tag == "@u": + user = value + continue + + if tag == "@c": + commit = value + continue + + if squash: + commit = rev_zero + + if not commits and commit != rev_zero: + continue + + value = value.replace("\\n", "\n") + + add_tag(tags, mr, commit, tag, user, value) + + if tag == "at": + value = datetime.datetime.utcfromtimestamp(int(value)) + add_tag(tags, mr, commit, "ai", user, value.isoformat()) + add_tag(tags, mr, commit, "ad", user, str(value)) + + if tag == "AT": + value = datetime.datetime.utcfromtimestamp(int(value)) + add_tag(tags, mr, commit, "AI", user, value.isoformat()) + add_tag(tags, mr, commit, "AD", user, str(value)) + + return tags + +def store_user_tags(tags, mr, user, fd): + fd.write("@m {}\n".format(mr)) + fd.write("@u {}\n".format(user)) + for commit, ctags in tags[mr].items(): + fd.write("@c {}\n".format(commit)) + for tag, values in ctags.items(): + for u, v in values: + fd.write("{} {}\n".format(tag, v)) + fd.write("\n") + +def load_user_tags(buf): + mr = "" + user = "" + commit = "" + tags = dict() + + for line in buf.split("\n"): + if not line: + continue + + sline = line.split(None, 1) + if len(sline) != 2: + sys.exit("Invalid tag line format '{}'.".format(line)) + + tag = sline[0].strip() + value = sline[1].strip() + if not value: + sys.exit("Empty tag value '{}'.".format(line)) + + if tag == "@m": + if mr: + sys.exit("More than one MR tag found '{}'.".format(line)) + mr = value + continue + + if tag == "@u": + if user: + sys.exit("More than one user tag found '{}'.".format(line)) + user = value + continue + + if tag == "@c": + commit = value + continue + + if not mr: + sys.exit("MR tag not found.") + + if not user: + sys.exit("User tag not found.") + + if not commit: + sys.exit("Commit tag not found.") + + if not tag.isalnum(): + sys.exit("Invalid tag name '{}'.".format(line)) + + add_tag(tags, mr, commit, tag, user, value) + + return tags + +def get_commit_tags(tags, mr, commit): + if mr not in tags: + return None + if commit not in tags[mr]: + return None + return tags[mr][commit] + +def get_tag(ctags, name): + if name not in ctags: + return None + return ctags[name] + +def get_tag_values(ctags, name, users=None): + tag = get_tag(ctags, name) + if not tag: + return [] + + if users: + return [v for u, v in tag if u in users] + else: + return [v for u, v in tag] + +def get_tag_value(ctags, name, user=None, default=""): + if user: + values = get_tag_values(ctags, name, users=[user]) + else: + values = get_tag_values(ctags, name) + if not values: + return default + + return values[0] + +def git_config(option): + rv, out, err = run(["git", "config", option], die=False) + if not rv: + return out.rstrip() + return "" + +def get_user(): + user = git_config("mr.user") + if not user: + sys.exit("No git-mr user found. Please use 'git config mr.user ', where is your username on server.") + return user + +def editor(text): + tmpdir = tempfile.TemporaryDirectory() + + editor = git_config("core.editor") + if not editor: + editor = os.environ.get('EDITOR','vi') + + fn = "{}/text".format(tmpdir.name) + with open(fn, mode="w") as f: + f.write(text) + + run([editor, fn], stdout=False, stderr=False) + + lines = list() + with open(fn, mode="r") as f: + for line in f.readlines(): + if line[0] == "#": + continue + lines.append(line) + + return "".join(lines) + +def format_patch(mr, mrtags, fd): + base = get_tag_value(mrtags, "B") + if not base: + return + + subj = get_tag_value(mrtags, "S") + desc = get_tag_value(mrtags, "D") + aname = get_tag_value(mrtags, "AN") + aemail = get_tag_value(mrtags, "AE") + date = get_tag_value(mrtags, "AD") + + cover = """{} +Message-ID: <{}.{}@localhost> +From: {} <{}> +Subject: [MR] {} +Date: {} +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +{} +""".format(from_line, mr, rev_zero, aname, aemail, subj, date, desc) + fd.write(cover.encode()) + + rv, out, err = run(["git", "--no-pager", "format-patch", "--stdout", + "--no-thread", "{}..{}".format(base, mr)], text=False) + + fromre = re.compile(b"From [0-9a-f]{40} Mon Sep 17 00:00:00 2001") + for line in out.split(b"\n"): + if fromre.match(line): + fd.write("{}\n".format(from_line).encode()) + rev = line.split(None, 2)[1].decode() + fd.write("Message-ID: <{}.{}@localhost>\n".format(mr, rev).encode()) + fd.write("In-Reply-To: <{}.{}@localhost>\n".format(mr, rev_zero).encode()) + else: + fd.write(line + b"\n") + +def store_mbox(mrs, oldrefs, currefs, newrefs, fd): + oldmrs = set(filter_revs(oldrefs, mrs=mrs, types=["head"])) + curmrs = set(filter_revs(currefs, mrs=mrs, types=["head"])) + newmrs = curmrs - oldmrs + + tags = load_tags(newmrs, refs=currefs, commits=False) + + for mr in newmrs: + mrtags = get_commit_tags(tags, mr, rev_zero) + if not mrtags: + continue + format_patch(mr, mrtags, fd) + + for mr in mrs: + oldmbs = set(filter_revs(oldrefs, mrs=[mr], types=["mbox"])) + curmbs = set(filter_revs(currefs, mrs=[mr], types=["mbox"])) + common = oldmbs & curmbs + oldmbs -= common + curmbs -= common + + if not curmbs: + continue + + stdin = "" + for rev in curmbs: + stdin += "{}\n".format(rev) + + for rev in oldmbs: + stdin += "^{}\n".format(rev) + + rv, out, err = run(["git", "rev-list", "--stdin"], stdin=stdin) + + fmt = "{}%n\ +From: %an <%ae>%n\ +Reply-To: %an <%ae>%n\ +Subject: %s%n\ +Date: %aD%n\ +Message-ID: <{}.%H@localhost>%n\ +MIME-Version: 1.0%n\ +Content-Type: text/plain; charset=UTF-8%n\ +Content-Transfer-Encoding: 8bit%n\ +%b".format(from_line, mr) + rv, out, err = run(["git", "show", "--format={}".format(fmt), "--stdin"], + stdin=out.encode(), text=False) + + fd.write(out) + + newrefs.clear() + newrefs += filter_refs(oldrefs, not_mrs=mrs, types=["head", "mbox"]) + newrefs += filter_refs(currefs, mrs=mrs, types=["head", "mbox"]) + +def commit_file(fn, name, msg, parent=None, msg_fn=None): + tmpdir = tempfile.TemporaryDirectory() + idxfn = "{}/index".format(tmpdir.name) + + if fn: + rv, out, err = run(["git", "hash-object", "-w", fn]) + fnsha = out.rstrip() + + rv, out, err = run(["git", "update-index", "--add", "--cacheinfo", "100644", + fnsha, name], env={"GIT_INDEX_FILE": idxfn}) + + rv, out, err = run(["git", "write-tree"], env={"GIT_INDEX_FILE": idxfn}) + treesha = out.rstrip() + + gitcmd = ["git", "commit-tree", treesha] + + if parent: + gitcmd += ["-p", parent] + + if msg: + gitcmd += ["-m", msg] + + if msg_fn: + gitcmd += ["-F", msg_fn] + + rv, out, err = run(gitcmd) + return out.rstrip() + +def update_ref(ref, commit=None, delete=False, oldrev=False): + cmd = ["git", "update-ref"] + + if delete: + cmd += ["-d", ref] + elif oldrev: + cmd += [ref, commit, oldrev] + else: + cmd += [ref, commit] + + rv, out, err = run(cmd) -- 1.7.10.4