--- /dev/null
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+ 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.
+
--- /dev/null
+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/<mr-sha-1>/<user>/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)]
--- /dev/null
+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)]
--- /dev/null
+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)]
--- /dev/null
+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)]
--- /dev/null
+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)]
--- /dev/null
+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)]
--- /dev/null
+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/<mr-sha-1>/<user>/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/<mr-sha-1>/<user>/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 <path> mr-sendmail"`::
+ Set proper <path> for git repo.
+
+SEE ALSO
+--------
+link:git-mr.html[git-mr(1)]
--- /dev/null
+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: <https://docs.python.org/3.2/library/string.html#formatstrings>
--- /dev/null
+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/<mr-sha-1>/<user>/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/<mr-sha-1>/<user>/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)]
--- /dev/null
+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)]
--- /dev/null
+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/<mr>/<username>/{head,tags,mbox}', where
+'<mr>' is MR's SHA-1 identifier, <username> 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/<mr>/<username>/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/<mr>/<username>/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)]
--- /dev/null
+.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)))
--- /dev/null
+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
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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)
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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="")
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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)
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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="")
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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)
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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)
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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)
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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)
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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: {} <ref> <oldrev> <newrev>".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()
--- /dev/null
+#!/usr/bin/env python3
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
+
+"""
+Copyright © 2020 Frantisek Hrbata <frantisek@hrbata.com>
+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 <user>', where <user> 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)