#!/bin/sh ### BuGit --- File-less distributed issue tracking system with Git # Copyright (C) 2015-2022 Stefan Monnier # URL: https://gitlab.com/monnier/bugit # Author: Stefan Monnier # Keywords: issue tracking system, bug tracking system, git # # This file is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # This file is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with GNU Emacs; see the file COPYING. If not, write to # the Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. ### Commentary ################################################################ # Interfacing with github for Bugs Everywhere: # https://gitlab.com/mcepl/bugseverywhere/tree/pull-from-github bugit_cmd_readme () \ { cat <<'ENDDOC' **BuGit**: File-less distributed issue tracking system with Git =============================================================== [**BuGit**](https://gitlab.com/monnier/bugit) is an *issue tracking system* (or *bug* tracking system) that stores its data in a [Git](https://git-scm.com) repository in such a way that not only is it possible to work offline, but the conflict resolution needed when sync'ing concurrent modifications is 99% performed automatically by Git itself rather than by BuGit (or by hand). To that end, most of the info is not stored in files but in Git's metadata and in file *names* instead. Furthermore, it aims to enable sophisticated distributed operation, where an issue may be shared between different databases. E.g. a GNU/Linux distribution may track an issue in its BuGit database and share that issue with the upstream developer's own BuGit database, so that changes in one show up in the other (while that issue may get different issue numbers in each database). Initial setup ------------- The normal way to set up a central repository would go something like: bugit init -- --bare bugit post-receive --install After which users can `git clone` the repository, work on it with the commandline BuGit client and then use `bugit sync` to merge their changes into the central repository. The repository's post-receive hook will take care of allocating numbers to issues and sending messages to the followers of an issue. Since BuGit uses branch names which all start with `bugit/` or `bugit-`, and since it does not make use of the work area, instead of cloning that repository into a fresh new repository, the user can also share the same repository with the main project. E.g.: cd ./myproject git remote add issues thehost:myproject-bugit-repository git config bugit.remote issues bugit sync Usage ----- You can get an overview of the commands supported by the command line client by running it without any argument (or with just `bugit help`): ENDDOC bugit_cmd_help 2>&1 | sed 's/^/ /' cat <<'ENDDOC' Furthermore, every subcommand comes with its own help: ENDDOC for cmd in $(bugit_commands); do echo "### BuGit $cmd"; echo # Run in a sub-shell since it calls 'exit'. ($(bugit_function "$cmd") --help) echo done cat <<'ENDDOC' Git config vars used by BuGit ----------------------------- Used by the command line BuGit client: - `user.name` - `user.email` - `bugit.remote`: Default to use for REMOTE arguments. - `bugit.usedescriptions`: If set to a non-empty string, new messages will be considered as the description of the issue. Used by the central post-receive script: - `bugit.mailhost`: host name to use for bugit's email addresses. - `bugit.mailuser`: user name to use for bugit's email addresses. - `bugit.sendmail`: command to use to send email. Defaults to `sendmail`. - `bugit.mailinglist`: email address to which we should send a copy of all new issue messages. - `bugit.bugurl`: prefix to add to an issue ID to get a valid URL. Design ------ The design is based on the idea of trying to represent the issue-database in such a way that Git's merge takes care of our own merge needs. IOW Git's merge should only result in conflicts when there is a *real* conflict that can only be resolved by hand (e.g. two concurrent changes to the title of an issue). The general idea is as follows: - Keep messages in the metadata (more specifically the commit log), so they can't generate conflicts, they're auto-merged, and the ordering automatically preserved. - Most other data is kept in file *names* (i.e. the files themselves are empty, to avoid merge conflicts). ### Issue identification Numbering issues is an inherently centralized operation, so in BuGit's distributed setting we need something more. In BuGit, issues can be identified in 3 ways: - **ID**: Issues have a unique and immutable identification called *id*, which is a random 32 digit lowercase-hexadecimal number. This is the only stable and unambiguous identification. - **NAME**: Issues have a *name*, also known as their *title*. This is a human-readable text which is expected to describe the issue concisely (max 200 bytes). This property can change over time, and several issues can have the same *name*, tho this should indicate that those issues should probably be merged. - **NB**: Issues can have a *number*. This is not a property of the issue, tho, in the sense that the same issue can be known under different numbers in different databases. So an issue can have several different NBs, and initially an issue has no NB at all (since allocation of an issue NB tends to be a centralized operation). But in a given database, a given issue has at most one NB. If BuGit says it wants a `ISSUE`, it means that you can give it any one of those kinds of identifiers. We could try to randomize the allocated number somewhat (basically add some random number to $nb), so that issue-numbering can be done offline as well! But the amount of randomization needed depends on the rate of new issues. E.g. Debian's debbugs seems to be getting around 100 new issues per day, so if we want to be able to "allocate today; push tomorrow" without having a high risk of collision, the amount of randomization needed is fairly high. As randomization increases, issue numbers end up looking more like issue IDs, so I'm not sure it's worth the trouble. ### Database layout The database is layed out as follows: - Every issue lives in its own branch named `bugit/`. The branch's commit messages hold the issue's messages (in Markdown format, tho without embedded HTML tags). The branch's files are as follows: - `name`: simple file holding the current *name* of this issue. - `description`: simple file which, if it exists, holds the description of the issue. - `attachments/ - ` are attachments. - `followers/` are empty files whose name indicates that `` would like to receive updates on this issue. - `owners/` are empty files whose name indicate that `` is reponsible for this issue. - `tags/` are empty files indicating that `` is applied to the issue. - `severity/` are empty files indicating that the severity of this issue should be considered as ``. Additionally to those issue branches, there are the following branches: - `bugit-master`: contains files of the form `numbers/` containing 33B holding the *id* of issue number `` plus LF. - `bugit-revmaps`: contains redundant reverse mappings. There are 2 kinds of such mappings: - `name//` to map issue *names* to the corresponding *id*. - `//` to map particular set values to the issues that have them. By design, the only possible sources of conflicts when merging different databases are: - if different issues have the same *number*. - if a given issue *id* has different names. - if an issue has two different attachments with the same name and same timestamp [ the timestamp should make this very unlikely, tho ]. ENDDOC :; } ### Helper functions ########################################################## bugit_debug () { : echo "DEBUG:" "$@" >&2; } bugit_run () { bugit_debug "$@"; "$@"; } bugit_getvar () { eval "echo \"\$$1\""; } bugit_setvar () { case $2 in *"'"* ) local quotedval="$(echo "$2" | sed "s/'/'\"'\"'/g")"; eval "$1='$quotedval'" ;; *) eval "$1='$2'";; esac } bugit_echo () { # POSIX `echo` has all kinds of "features" we don't want, such as # handling of \c and -n. cat <&2 } bugit_oneline_summary () { args="" for opt in $1; do case $opt in *\= ) argname=${opt%=} args="$args [--$argname=$(bugit_upcase "$argname")]" ;; * ) args="$args [--$opt]" ;; esac done shift; echo "$args" $(bugit_upcase "$@") } bugit_make_optloop () { args_summary=$(bugit_oneline_summary "$@") echo 'done=""' echo "args_summary='$args_summary'" echo 'while [ $# -gt 0 ] && [ "" = "$done" ]; do' echo 'case $1 in' echo '--help) echo " bugit $cmd" $args_summary; echo; cat; exit 0;;' for arg in $(echo $1); do case $arg in *\=) argname=${arg%=} echo "--${arg}*) ${argname}=\${1#*=}; shift ;;" echo "--${argname})" echo "[ \$# -gt 1 ] || invalid \"Missing arg for \$1 option\"" echo "${argname}=\$2; shift 2 ;;" ;; *) echo "--${arg}) ${arg}=true; shift ;;" ;; esac done echo '--) done=--; shift ;;' echo '--*) invalid "Invalid option $1" ;;' echo '*) done=-- ;;' echo 'esac' echo 'done' shift while [ $# -gt 0 ]; do arg=$1 case $arg in *"..."*) [ $# = 1 ] || internal_error "... can only be last" rest=ok ;; "["*"]") argname=$(echo "$arg" | sed 's/[][]//g') echo "[ \$# = 0 ] || { $argname=\$1 ; shift; }" ;; *) echo "[ \$# -gt 0 ] ||" echo "invalid 'Missing argument $(bugit_upcase "$arg")'" echo "{ $arg=\$1 ; shift; }" ;; esac shift done [ "ok" = "$rest" ] || echo "[ \$# = 0 ] || invalid 'Unexpected extra args:' \"\$@\"" } bugit_commands () { # FIXME: I wish there was a more reliable way to get this! local source="$(which "$0")" sed -ne 's|^bugit_cmd_\([^ ()]*\).*() *{.*|\1|p' <"$source" | tr '_' '-' } bugit_function () { # (CMD): Name of the function that implements CMD. echo "bugit_cmd_$1" | tr '-' '_' } invalid () { { echo "$@"; echo for cmd in $(bugit_commands); do ($(bugit_function "$cmd") --help | head -n 1 | sed 's/^ *//') done } >&2 exit 1 } user_error () { echo "ERROR: $@" >&2 exit 1 } internal_error () { user_error "INTERNAL:" "$@"; } bugit_dir_names () { # Select the names part of "git cat-file -p" output. sed 's/^.\{53\}//' } bugit_get_id () { local issue="$1" [ ! "" = "$issue" ] || user_error "Empty issue identifer!" if case $1 in *[!a-f0-9]* ) false;; *) [ "${#issue}" = 32 ];; esac; then # FIXME: on the debbugs-combined repository: show-ref takes 100x # more time than rev-parse. if git show-ref "bugit/$issue" >/dev/null; then id="$issue" else user_error "No issue with this ID" fi elif case $issue in *[!0-9]* ) false;; *) true;; esac; then if id=$(git cat-file blob \ "refs/heads/bugit-master:numbers/$issue" 2>/dev/null); then bugit_table_set number "$id" "$issue" else user_error "No issue with this number" fi else local file="$(bugit_to_filename "$issue")" local ids ids=$( (git cat-file -p \ "refs/heads/bugit-revmaps:name/$file" 2>/dev/null || git cat-file -p "refs/heads/bugit-revmaps:name" | while read _mode _type _hash name; do case $name in *"$file"* ) git cat-file -p \ "refs/heads/bugit-revmaps:name/$name" ;; esac done) | bugit_dir_names) set -- $ids case $# in 0) user_error "No issue by that name" ;; 1) id=$1 ;; *) user_error "Ambiguous name '$issue': $(bugit_cmd_list "$@" | sed 's/^/ /')" ;; esac fi } bugit_expand_file_name () { case "$1" in /* ) echo "$1" ;; * ) echo "$(pwd)/$1" ;; esac } bugit_repository=$(bugit_expand_file_name \ "$(git rev-parse --git-dir 2>/dev/null)") bugit_dir () { dir=$bugit_repository/bugit bugit_mkdir "$dir" echo "$dir" } bugit_get_number () { # bugit_debug "Getting number for '$1'!" number=$(bugit_table_get number "$1") [ "" != "$number" ] || { [ "done" != "$(bugit_table_get number init)" ] && { local ids bugit_get_numbered_ids number=$(bugit_table_get number "$1") [ "" != "$number" ] } } } bugit_get_name () { local id="$1" bugit_get_branch "$id" name=$(git cat-file blob "$branch:name") } bugit_assert_clean_p () { [ "" = "$(git status --porcelain)" ] || user_error "Uncommitted changes!" } # Usually, BuGit works without relying on a checkout at all (and doesn't # keep the work-area up-to-date, so it's better not to have a checkout of # BuGit's branches), but there is one operation for which we crucially need # a checkout: when merging changes from other (typically remote tracking) # branches. For those cases, we create a temporary checkout area using # `git worktree`. bugit_checkout () { if [ -d "$1" ]; then (cd "$1"; bugit_assert_clean_p; git checkout "$2") else (cd "$bugit_repository"; git worktree add "$1" "$2") fi } bugit_get_branch () { # Find the branch of a given issue-id bugit__get_branch "bugit/$1" "$2" "$3" [ "" != "$branch" ] || user_error "No issue with id '$1'" } bugit__get_branch () { # Find the branch of a given issue-id name=$1 option=$2 # Can be "rw" or "merge" dir=$3 # Only used for "merge" if git show-ref -q --verify "refs/heads/$name"; then branch="refs/heads/$name" else branches=$(git for-each-ref --format "%(refname)" \ "refs/remotes/*/$name") set -- $branches case $# in 0) : ;; 1) if [ "rw" != "$option" ]; then branch=$1 else git branch --no-track "$name" "$1" branch="refs/heads/$name" fi ;; *) first="$1"; shift if [ "merge" = "$option" ]; then checkout=$dir else checkout=$(bugit_dir)/checkout-$$ bugit_schedule_rm "$checkout" fi git branch --no-track "$name" "$first" bugit_checkout "$checkout" "$name" local cwd="$(pwd)" cd "$checkout" for other; do bugit_merge "$name" "$other"; done cd "$cwd" branch="refs/heads/$name" ;; esac fi } bugit_merge () { issue=$1 branch=$2 git merge -m Merge "$branch" || user_error "Issue '$issue' requires manual merging" } bugit_fast_import () { local cmds="$(cat)" local i=1 # Here, we assume that Git only exits with a status of 1 if it encountered # a transient error which will likely disappear if we try again (i.e. there # was a concurrent update on the same branch). until echo "$cmds" | git fast-import --quiet || [ "$?" != 1 ] || [ "$i" -gt 100 ]; do sleep $(($RANDOM % $i)) i=$(($i + $i)) bugit_debug "Retrying git fast-import because of concurrent updates" done } bugit_author_date () { git var GIT_COMMITTER_IDENT } bugit_author () { local author_date="$(bugit_author_date)" echo "${author_date%>*}>" } bugit_to_filename () { case $1 in "" ) user_error "Empty element!" ;; *[\ \ ] | [\ \ ]* ) # Reject it, since "read mode type dataref name" would mis-read it. user_error "Leading|trailing space in name '$1'" ;; *[%/]* ) echo "$1" | sed -e 's/%/%25/g' -e 's|/|%2f|g' ;; * ) echo "$1" ;; esac } bugit_from_filename () { case $1 in "" ) user_error "Empty element!" ;; *%* ) echo "$1" | sed -e 's|%2f|/|g' -e 's|%25|%|g' ;; * ) echo "$1" ;; esac } bugit_mkdir () { # Like "mkdir -p" but only for a single level. [ -d "$1" ] || mkdir "$1" } bugit_generate_id () { (uuidgen || dd bs=1 count=16 /dev/null | sed -e 's/[- ]//g' -e 'y/ABCDEF/abcdef/' } bugit_add_attachments () { for attachment; do filename="$(date "+%Y-%m-%d %H:%M") - $(basename "$attachment")" echo "M 644 inline attachments/$filename" echo "data $(wc -c <"$attachment")" cat "$attachment"; echo done } # Elements (surrounded by spaces) that can appear in the top-level directory # of an issue and which are not "set"s. Everything else is assumed to be # a set (represented by a directory holding empty files). bugit_nonsets=" name attachments " bugit_manage_set () { # SET ISSUE [OPERATION] [VALS...] set=$1 issue=$2 operation=$3 shift 3 case $set in *" "* ) user_error "Invalid set name '$set'";; esac bugit_get_id "$issue" if [ "" = "$operation" ]; then bugit_get_branch "$id" rw git cat-file -p "$branch:$set" 2>/dev/null | while read _mode _type _hash name; do echo "$(bugit_from_filename "$name")" done else bugit_change_set_cmds "$set" "$operation" "$@" [ "" != "$cmds_main" ] || return 0 git fast-import --quiet </dev/null | while read _mode _type _hash name; do echo "D $set/$name/$id" done) else cmds_main="#Don't end this commit yet" cmds_rev="#Don't end this commit yet" fi for x; do cmds_main="$cmds_main M 644 inline $set/$(bugit_to_filename "$x") data 0 " cmds_rev="$cmds_rev M 644 inline $set/$(bugit_to_filename "$x")/$id data 0 " done ;; "-" ) cmds_main="#Don't end this commit yet" cmds_rev="#Don't end this commit yet" for x; do cmds_main="$cmds_main D $set/$(bugit_to_filename "$x")" cmds_rev="$cmds_main D $set/$(bugit_to_filename "$x")/$id" done ;; * ) user_error "Set operation '$operation' not among +/-/=" ;; esac } bugit_sanitize_name () { local name="$1"; local error; if [ "" = "$2" ]; then error=bugit_debug; else error=user_error; fi [ "${#name}" -gt 0 ] || { $error "Issue name empty!"; name="_"; } [ "${#name}" -le 200 ] || { $error "Issue name too long! (max 200 bytes)" name=$(echo "$name" | cut -c1-200) } case $name in *[![:alpha:]\ \ -@\[-\`{-~]*) $error "Issue name has unsupported chars! (newline or control chars)" name=$(echo "$name" | tr '\000-\010\012-\037' '[ *]') ;; esac echo "$name" } bugit_sanitize_author () { local error; if [ "" = "$2" ]; then error=bugit_debug; else error=$2; fi case $1 in *" <"*@*">" ) echo "$1" ;; * ) $error "Author not in 'Name ' format" local host="${1#*@}" host=${host%% *} host=${host%>} local user="${1%%@*}" user=${user##* } user=${user#<} echo "$user <$user@$host>" esac } bugit_urldecode () { # FIXME: We need to decode the uri fields we get as arguments. # This turns out to be farily nasty: according to my tests, things # like "ŕ" get encoded in two steps: first it's turned into ŕ # and then each one of &, #, and ; get %-encoded. local str="$1" # OK, so let's first take care of the %-encoded element. case $str in # First we need to percent-decode (i.e. get rid of the %NN). *%* ) str=$(bugit_echo "$str" | sed -e 's/\\/\\\\/g' -e 's/%%/%25/g' \ -e 's/%\([[:xdigit:]][[:xdigit:]]\)/\\x\1/g' \ -e 's/%/%%/g') # Dash's built-in printf doesn't support \xNN ! str=$(/usr/bin/printf "$str") ;; esac # Now take care of the decimal HTML character references. case $str in *"&#"[0-9]*";"* ) # Yay! Dash's built-in printf does support %x! str=$(bugit_echo "$str" | sed -e 's/\\/\\\\/g' \ -e "s/'/'\\\''/g" \ -e "s|&#\\([0-9][0-9]*\\);|\\&#x'\$(printf \"%08x\" \\1)';|g") # Dash's builtin `echo` doesn't support -E. str=$(eval "bugit_echo '$str'") ;; esac # # Now take care of the hexadecimal HTML character references. case $str in *"&#x"*";"* ) str=$(bugit_echo "$str" | sed -e 's/\\/\\\\/g' \ -e 's/%/%%/g' \ -e "s|&#x\\([0-9a-fA-F]\\{2\\}\\);|\\\u00\\1|g" \ -e "s|&#x\\([0-9a-fA-F]\\{4\\}\\);|\\\u\\1|g" \ -e "s|&#x\\([0-9a-fA-F]\\{8\\}\\);|\\\U\\1|g") # Dash's built-in printf doesn't support \uNNNN ! str=$(/usr/bin/printf "$str") ;; esac bugit_echo "$str" } bugit_record_new_name () { id=$1; new=$2; old=$3 bugit__get_branch "bugit-revmaps" "rw" # Record the name->id mapping in the master branch. bugit_debug "bugit_record_new_name '$1' '$2' '$3'" git fast-import --quiet <"$msgfile" $(git config core.editor || echo ${EDITOR:-vi}) "$msgfile" message=$(grep -v "^|BUGIT|" <"$msgfile") case $message in *[[:alnum:]]* ) : ;; * ) user_error "No message! Aborting!" esac } bugit_map_numbered_ids () { # FUN # Return all numbered IDs, and remember their number. local fun="$1" local nbs="$(mktemp)" bugit_schedule_rm "$nbs" local id number # FIXME: Make this work with numbers/N which contain several IDs? bugit_debug "Starting bugit_get_numbered_ids" # We used to use "git fast-import" (instead of git cat-file --batch) # with commands "progress $name; cat-blob $sha", so the output had both # ids and numbers together, rather than the current scheme where we have # two files and we have to hope that they're in sync. # This works OK as long as you have write-access to the repository, # but breaks miserably when you only have read-only access: even though # we don't create any new element, "git fast-import" insists on creating # a new "object/pack/tmp_pack_XXX" file before going any further! ids=$(git cat-file -p refs/heads/bugit-master:numbers 2>/dev/null | while read _mode _type sha name; do echo "$name" >&3 echo "$sha" done 3>"$nbs" | git cat-file --batch | grep -v '^$\| blob ') bugit_debug "Processing git-fast-import's ids&numbers" # Be careful to call bugit_table_set operate in the right process! # This loop takes about twice as much time as the one above, in my # experience, so there's room for improvement! for id in $ids; do read number && bugit_table_set_fast number "$id" "$number" $fun "$id" "$number" done <"$nbs" bugit_table_set number init done bugit_debug "Done with bugit_get_numbered_ids" } bugit_get_numbered_ids () { bugit_map_numbered_ids : } bugit_mime_type () { # FILENAME [DEFAULT-TYPE] local ce ext ext=${1##*.} case $ext in Z | gz | bz2 | xz ) case $ext in Z ) ce=compress ;; gz ) ce=gzip ;; bz2 ) ce=bzip2 ;; xz ) ce=xz ;; esac ce="Content-Encoding: $ce" ext=$(1%.*) ext=${ext##*.} ;; esac while read type exts; do case $type in "#"* ) : ;; * ) for candidate in $exts; do if [ "x$ext" = "x$candidate" ]; then mime_type=$type fi done ;; esac done /dev/null committer=$(bugit_author) bugit_sanitize_author "$committer" user_error >/dev/null if [ "" = "$author" ]; then author=$committer else bugit_sanitize_author "$author" user_error >/dev/null fi id=$(bugit_generate_id) bugit_read_message \ "|BUGIT| Describe the issue, explaining what you've done, what you expected |BUGIT| to happen and what happened instead. |BUGIT| BuGit assumes this is written in Markdown format." local date="$(date '+%s %z')" if [ "" = "$usedesc" ]; then usedesc=$(git config bugit.usedescriptions 2>/dev/null); fi git fast-import --quiet </dev/null || { git log --first-parent --reverse "$branch" | sed -e '1,/^$/d' -e '/^commit/,$d' -e 's/^ //' } } bugit_cmd_reply () { eval "$(bugit_make_optloop 'author= editdesc' issue [attachment...])" <<'ENDDOC' Add information to issue ISSUE (can be an issue's NAME, ID or NUMBER). Just like `bugit new`, it accepts attachments and will bring up an editor to let you write a message. With option `--editdesc`, this will let you edit the description of the issue instead of adding an extra message. ENDDOC committer=$(bugit_author) bugit_sanitize_author "$committer" user_error >/dev/null if [ "" = "$author" ]; then author=$committer else bugit_sanitize_author "$author" user_error >/dev/null fi bugit_get_id "$issue" bugit_get_name "$id" bugit_get_number "$id" bugit_read_message \ "|BUGIT| $(if [ "" = "$editdesc" ]; then echo "Enter additional information" else echo "Edit the description below"; fi) about BuGit#${number:-$id} |BUGIT| titled '$name'. |BUGIT| BuGit assumes this is written in Markdown format. $(if [ "" != "$editdesc" ]; then bugit_cmd_description "$id"; fi)" local date="$(date '+%s %z')" git fast-import --quiet <&1); then # bugit_invert_grep_support=true # else # case $out in # *"--invert-grep"* ) bugit_invert_grep_support=false ;; # esac # return 1 # fi ;; # esac # } bugit_omit_internal_commits () { sed -n -e ' /^commit /{ : foundcommit h : readingheader n /^$/{ H n /^ $/{ : omitinternal n /^commit /b foundcommit b omitinternal } x p x b exit } H b readingheader } : exit p ' } bugit_show_messages () { local meta="$1" revisions="$2" opts="" filter="cat" if [ "true" = "$meta" ]; then filter="grep -v ^....$bugit_internal_marker\$" # elif bugit_invert_grep_supported_p; then # opts="--grep ^$bugit_internal_marker\$ --invert-grep" else filter=bugit_omit_internal_commits fi dhdr=$(git log --no-merges "$revisions" -- description | awk ' BEGIN { FS="^[^: ]*: *" } /^commit / { if (commit == "") commit=$0 } #The most recent commit. /^Date:/ { date=$0 } #The oldest date. /^Author:/ { if (authors == "") authors=$2; #All the authors. else if (!index (authors, $2)) authors=$2", "authors } END { if (commit != "" ) { print commit; print "Author: " authors; print date } } ') if [ "" != "$dhdr" ]; then echo "$dhdr"; echo case "$revisions" in *..*) git diff "$revisions" -- description ;; refs/* ) git cat-file -p "$revisions:description" | sed 's/^/ /'; # FIXME: Depending on "description"'s content we may need # anywhere betwen 0 and 2 extra newlines. echo; echo ;; * ) internal_error "Unexpected revisions '$revisions'" ;; esac fi bugit_run git log --no-merges --reverse $opts "$revisions" | $filter } bugit_cmd_show () { eval "$(bugit_make_optloop 'meta' issue)" <<'ENDDOC' Display the messages in issue ISSUE. If the `--meta` option is provided, the list will include messages about changes to the meta-data, such as changes to the tags, name, severity, ... ENDDOC bugit_get_id "$issue" bugit_get_branch "$id" bugit_debug "issue '$issue' has branch '$branch'" bugit_show_messages "$meta" "$branch" } bugit_accumulate_ids () { echo "$1"; } bugit_cmd_list () { eval "$(bugit_make_optloop '' [issue...])" <<'ENDDOC' List existing issues. If no ISSUE argument is provided, list all non-closed issues in the database. Otherwise, only list the provided issues. ENDDOC local ids if [ $# = 0 ]; then local nbs="$(mktemp)" bugit_schedule_rm "$nbs" bugit_map_numbered_ids bugit_accumulate_ids >"$nbs" ids=$( (cat "$nbs"; git for-each-ref --format "%(refname)" 'refs/heads/bugit/*' | sed 's|.*/||') | sort -u) closed=$(git cat-file -p refs/heads/bugit-revmaps:tags/closed \ 2>/dev/null | bugit_dir_names) for id in $ids; do case $closed in *"$id"* ) : ;; * ) echo "$id" ;; esac done >"$nbs" ids=$(cat "$nbs") else ids="" for issue; do bugit_get_id "$issue"; ids="$ids $id" done fi for id in $ids; do bugit_get_name "$id" || name="" bugit_get_number "$id" echo "bugit#${number:-$id}: $name" done | sort -n -k 2 -t "#" } bugit_cmd_refresh_revmaps () \ { eval "$(bugit_make_optloop '')" <<'ENDDOC' Refresh the Names->ID reverse map. ENDDOC local fifo="$(bugit_dir)/refresh-names-$$" bugit_schedule_rm "$fifo" bugit__get_branch "bugit-revmaps" "rw" mkfifo "$fifo" { cat < in git@vger.kernel.org # we could use "git cat-file --batch" here, tho it only gives me # non-pretty printed tree blobs, so we'd have to parse those # according to the following format: # tree = tree_entry* # tree_entry = mode SP path NUL sha1 # mode = ascii mode, in octal (e.g., "100644") # path = * # sha1 = {20} while read refname objectname; do id=${refname##*/} git cat-file -p "$refname:" | while read _mode _type dataref name; do case $bugit_nonsets in *" $name "* ) ;; * ) # This is a set. bugit_debug "Found set $name" git cat-file -p "$dataref" | while read _mode _type _dataref elemname; do echo "M 644 inline $name/$elemname/$id" echo "data 0"; echo done ;; esac done echo "ls $objectname name" read mode _type dataref _name <&4 if [ "missing" = "$mode" ]; then continue; fi issuename=$(bugit_cat_blob "$dataref" <&4) 3>&1 filename=$(bugit_to_filename "$issuename") echo "M 644 inline name/$filename/$id" echo "data 0"; echo done cat <"$fifo" } bugit_cat_blob () { # fd#0git's input, fd#1>result local sha1 blob size emptyline echo "cat-blob $1" >&3 read sha1 blob size dd status=none bs=1 "count=$size" read emptyline [ "" = "$emptyline" ] || internal_error "No LF after blob" } bugit_cmd_allocate () { # FIXME: Using empty files, like we do elsewhere, works usually well in # terms of efficiency, but using small files like all the "numbers/" # we generate tends to be very wasteful. E.g. my Debian GNU/Linux system # uses 4KB per file, even though it only holds a 32B string (a factor # 100x of inefficiency). For 10'000 issues, that means 40MB dedicated to # mapping the NBs to the IDs, which sucks when you think that it's easy # to fit into 320KB. # For this reason, we should consolidate "old" issue numbers into larger # files (after all, the main reason to use individual files is to ease # up handling of conflicts, but there shouldn't be any for old issues). eval "$(bugit_make_optloop '' [issue...])" <<'ENDDOC' Allocate issue numbers to the ISSUEs listed on the command line. Upon creation, issues only have an ID and a NAME but not a NUMBER, since allocating a number cannot be done reliably offline. This command is the one that will allocate a number for the provided issues. In order for it to work well and not risk introducing conflicts, this should normally be run on the central repository, typically from the post-receive hook. ENDDOC [ $# -gt 0 ] || # FIXME: when no ISSUE is specified, we could do it for all # un-numbered local issues. user_error "Have to identify issues explicitly" ids="" for issue; do bugit_get_id "$issue" # Check that this issue doesn't already have a number. if bugit_get_number "$id"; then echo "Already assigned number $number to issue '$issue'" continue else ids="$ids $id" fi done if [ "" = "$ids" ]; then return; fi # Now allocate the new numbers. bugit_allocate_numbers $ids } bugit_allocate_numbers () { local repetition=1 local fifo="$(bugit_dir)/allocate-$$" local tmpfile="$(mktemp)" bugit_schedule_rm "$tmpfile" # Get a starting number. We can assume (and should ensure) that all # numbers before this one are already allocated. local start_file="$(bugit_dir)/allocated" local nb="$(cat "$start_file" 2>/dev/null || git config bugit.minnb)" [ "" != "$nb" ] || nb=1 bugit_debug "start_file='$start_file' nb='$nb'" local first=true local i id idline lastnb mkfifo "$fifo" bugit_schedule_rm "$fifo" until { cat <&3 cat <"$tmpfile" | git fast-import --quiet --cat-blob-fd=3 3>"$fifo" || [ "$?" != 1 ] || [ "$repetition" -gt 10 ]; do echo -n >"$tmpfile" sleep $(($RANDOM % ($repetition * $repetition))) bugit_debug "Retrying allocation because of concurrent updates" done while read nb id; do lastnb=$nb bugit_table_set number "$id" "$nb" done <"$tmpfile" # Try to avoid concurrent writes to $start_file. bugit_debug "Saving start=$lastnb" echo "$lastnb" >"$start_file.$$" mv "$start_file.$$" "$start_file" } bugit_cmd_sync () { eval "$(bugit_make_optloop 'push pull subset postreceive' [remote] [issue...])" <<'EOD' Sync the local repository with the repository REMOTE. REMOTE defaults to `origin` or the value of `bugit.remote`. By default, this pulls and pushes all new changes, including new messages, new issues, and new issue numbers. With option `--pull`, this only pulls the remote changes. With option `--push`, this only pushes the changes to the remote repository. With option `--subset`, issue numbers are not propagated and only those issues which already exist on REMOTE will be updated. With option `--postreceive`, run the local post-receive hooks. This is needed when running `bugit sync` from the master repository. Finally if ISSUEs are provided, then only those issues's info are sync'd. REMOTE is a name set up with `git remote` (which see). EOD [ ! "" = "$remote" ] || remote=$(git config bugit.remote || echo origin) checkout=$(bugit_dir)/checkout-$$ bugit_schedule_rm "$checkout" # Note: We create the checkout lazily rather than doing it here. # This is because creating the checkout for an issue is cheap, # but we don't know yet which branch to checkout, and the only ones we # know exist (bugit-revmaps and bugit-master) tend to be more costly to # checkout and may not need to be checked out at all. if [ "true" = "$subset" ] && [ $# -gt 0 ]; then invalid "'--subset' is meaningless with explicit ISSUEs" elif [ "" = "$push$pull" ]; then push=true; pull=true fi if [ "true" = "$pull" ]; then if [ "true" = "$postreceive" ]; then bugit_do_pushlike_pull "$@" else bugit_do_pull "$checkout" "$@" fi fi if [ "true" = "$push" ]; then bugit_do_push "$@" # The push may have caused new changes in the remote repository # via the post-receive hook (typically, allocation of issue numbers to # the new issues). # So we should re-pull and update "bugit-master". if [ "true0" = "$pull$#$subset" -a "true" != "$postreceive" ]; then bugit_debug "Re-pulling in case of new issue numbers" # FIXME: Use "git fetch" and "git fast-import" to perform the # fast-forward pull. # FIXME: Not sure if the "git fetch" is even needed, since I got # the impression that maybe the push already updated the # remote-tracking branch. local cwd="$(pwd)" bugit_checkout "$checkout" "bugit-master" cd "$checkout" git pull --no-edit "$remote" bugit-master:bugit-master cd "$cwd" fi fi } bugit_do_push () { if [ "true" = "$subset" ]; then # For some reason that escapes me, 'bugit/*:bugit/*' doesn't actually # update the branches, whereas it works with the additional # "refs/heads/" prefixes! git push "$remote" 'refs/heads/bugit/*:refs/heads/bugit/*' elif [ $# = 0 ]; then locals=$(git for-each-ref --format "%(refname)" "refs/heads/bugit/*" | sed 's|.*/||') remotes=$(git for-each-ref --format "%(refname)" \ "refs/remotes/$remote/bugit/*" | sed 's|.*/||') new=$( (echo "$locals"; echo "$remotes"; echo "$remotes") | sort | uniq -u) extra_refspecs=$(echo "$new" | sed 's|\(..*\)|bugit/\1:bugit/\1|') if [ "true" = "$postreceive" ]; then extra_refspecs="$extra_refspecs refs/heads/bugit-master" extra_refspecs="$extra_refspecs refs/heads/bugit-revmaps" fi # For some reason that escapes me, 'bugit/*:bugit/*' doesn't actually # update the branches, whereas it works with the additional # "refs/heads/" prefixes! bugit_run git push "$remote" 'refs/heads/bugit/*:refs/heads/bugit/*' \ $extra_refspecs else refspecs="" for issue; do bugit_get_id "$issue" refspecs="$refspecs bugit/$id:bugit/$id" done git push "$remote" $refspecs fi } bugit_do_pushlike_pull () { local mirror="$(bugit_dir)/mirror.$$" bugit_schedule_rm "$mirror" # FIXME: "git remote get-url" is apparently not present yet in Debian # stable's Git (2.1.4). local url="$(git config "remote.$remote.url")" git clone --mirror --reference "$bugit_repository" "$url" "$mirror" (cd "$mirror" && remote="dest" && git remote add "$remote" "$bugit_repository" && bugit_cmd_sync "$@") } bugit_do_pull () { local checkout="$1"; shift pre="refs/remotes/$remote/" if [ "true" = "$subset" ]; then patterns="${pre}bugit/ ${pre}/bugit-revmaps" elif [ $# = 0 ]; then patterns="${pre}" else patterns= for issue; do bugit_get_id "$issue" patterns="$patterns ${pre}bugit/$id" done fi git fetch "$remote" # FIXME: Rather than loop over all refs in the remote, we should loop # over all local refs. # FIXME: And we should delete local refs if they are identical to the # remote ref (and they have an issue-number). for branch in $(git for-each-ref --format "%(refname)" $patterns); do local=${branch#refs/remotes/*/} if [ "HEAD" = "$local" ]; then bugit_debug "Skipping HEAD" elif ! git show-ref -q --verify "refs/heads/$local"; then : bugit_debug "branch $branch has no local equivalent '$local'" elif [ "" = "$(git rev-list "$local..$branch")" ]; then : bugit_debug "branch $branch has nothing new" else local cwd="$(pwd)" bugit_checkout "$checkout" "$local" cd "$checkout" bugit_merge "$local" "$branch" cd "$cwd" fi done } bugit_cmd_merge () { eval "$(bugit_make_optloop '' issue dir)" <<'ENDDOC' Merge the various branches existing for ISSUE. DIR is the target directory into which the conflicted branch will be checked out and where conflicts (if any) will need to be resolved manually. ENDDOC bugit_get_id "$issue" bugit_get_branch "$id" merge "$dir" } bugit_cmd_id () { eval "$(bugit_make_optloop '' issue)" <<'ENDDOC' Return the ID of the given ISSUE. ENDDOC bugit_get_id "$issue" echo "$id" } bugit_cmd_number () { eval "$(bugit_make_optloop '' issue)" <<'ENDDOC' Return the NUMBER allocated to the given ISSUE. ENDDOC bugit_get_id "$issue" if bugit_get_number "$id"; then echo "$number" else user_error "Issue '$issue' has no number"; fi } bugit_cmd_name () { eval "$(bugit_make_optloop '' issue [newname])" <<'ENDDOC' If NEWNAME is provided, update the NAME of ISSUE to NEWNAME. Otherwise, just return the NAME of ISSUE. ENDDOC bugit_get_id "$issue" bugit_get_name "$id" if [ "" = "$newname" ]; then echo "$name" elif [ "x$name" = "x$newname" ]; then echo "Name is unchanged" >&2 else bugit_sanitize_name "$newname" error >/dev/null git fast-import --quiet <" } bugit_mua_getadd_msg_id () { local nb="$1" newid="$2" local msgids msgid rev="" # Save a "NUMBER->MSGIDS" map in files, grouping several issues per file # and keeping only the last few bytes of message-ids for each issue. local issues_per_file=256 B_per_issue=256 local filename="bugit-msgids/$(($nb / $issues_per_file))" local record="$(($nb % $issues_per_file))" msgids=$(dd status=none cbs=$B_per_issue bs=$B_per_issue \ conv=unblock skip="$record" count=1 if="$filename" 2>/dev/null | tr '\000' ' ') for msgid in ${msgids% *}; do # Strip off an incomplete trailing msgid! if [ "" = "$rev" ]; then rev=$msgid else rev="$msgid $rev" # Output in reverse order (oldest first)! fi done echo "$rev" if [ "" != "$newid" ]; then bugit_mkdir bugit-msgids echo "$newid $msgids" | dd status=none cbs=$B_per_issue bs=$B_per_issue \ conv=block seek="$record" count=1 of="$filename" fi } bugit_mua_gitlog_to_markdown () { # FIXME: We should just tell Git to generate this format directly. sed -n -e '/^commit /d' \ -e 's/^[[:alpha:]]*:/## &/p' \ -e 's/^ //p' \ -e '/^$/p' } bugit_mua_sendmail () { mailhost=$(git config bugit.mailhost) mailuser=$(git config bugit.mailuser || echo bugit) sendmail=$(git config bugit.sendmail || echo sendmail) OLDIFS="$IFS"; IFS=" "; set -- $1 IFS="$OLDIFS" bugit_run "$sendmail" -f "$mailuser-owner@$mailhost" "$@" } bugit_compose_email () { local number="$1" id="$2" subject="$3" attachments="$4" rev="$5" local url="$(git config bugit.bugurl)" local mailuser="$(git config bugit.mailuser || echo bugit)" local mailhost="$(git config bugit.mailhost)" local msgid="$(bugit_mua_generate_msg_id)" local last_msgids="$(bugit_mua_getadd_msg_id "$number" "$msgid")" cat <&2 for alloc; do rev="${alloc%=*}" id="${alloc#*=}" bugit_get_number "$id" >&2 bugit_get_name "$id" followers="$(bugit_cmd_follow "$id")" bugit_mua_send_acknowledgment "$number" "$name" "$id" "$followers" bugit_mua_notify_newmsgs "$id" "" "$rev" done } bugit_postreceive_revmaps () { local id="$1" diff="$1" name="$2" oldrev="$3" echo "$diff" | { cat <.... sed -e ' /^commit /{ s/.*// : header n s|^\([^:]*: *\)\(.*[^ ]\) *<\(.*@.*\)>|\1\2| s|^\([^:]*\):\(.*\)| | t header s|.*|
\1:\2
\ | } t s/^ // #No embedded HTML. s//dev/null || markdown () { echo "

¡¡Missing \"markdown\" executable!!

" cat } bugit_cgi_quote () { sed -e 's/&/\&/g' -e 's//\>/g' } bug_cgi_http_header () { cat < $1

$1

EOD } bugit_cgi_footer () { cat <<'EOD'
EOD } bugit_cgi_fail () { bug_cgi_http_header bug_cgi_html_head "Test CGI output" cat < EOD for v in HTTP_USER_AGENT HTTP_HOST SCRIPT_FILENAME REQUEST_URI \ SCRIPT_NAME REMOTE_PORT HTTP_ACCEPT_LANGUAGE HTTP_ACCEPT \ REMOTE_ADDR SERVER_SOFTWARE QUERY_STRING GATEWAY_INTERFACE \ SERVER_PROTOCOL REQUEST_METHOD; do echo "$v=$(bugit_getvar "$v")" # echo "$v" done cat < ENDDOC bugit_cgi_footer } bugit_cgi_show () { bug_cgi_http_header issue=$1 bugit_get_id "$issue" bugit_get_number "$id" bugit_get_name "$id" local qname="$(echo "$name" | bugit_cgi_quote)" bug_cgi_html_head "BuGit#$number: $qname" tags=$(bugit_manage_set tags "$id" "") if [ "" != "$tags" ]; then # FIXME: enrich those tag words, e.g. to make them links to search # for other issues with that tag, or to togge that tag, or to pop an # explanation about what the tag means, ... echo "

Tags: " $tags "

" fi attachments=$(bugit_cmd_attachment "$id") if [ "" != "$attachments" ]; then echo "

Attachments:" # FIXME: URL-encode! # FIXME: Fix hardcoded "bugit.cgi?"! echo "$attachments" | sed "s|^\\(.*\\)\$|\\1
|" echo "

" fi bugit_cmd_show "$id" | bugit_cgi_gitlog_to_html bugit_cgi_footer } bugit_cgi_show_attachment () { local id="$1" attachment="$2" # We default to text/plain rather than application/octet-stream because # it is common to have attachments in the form of logs or # sample-sessions which don't have standardized extensions and where # application/octet-stream would prevent the browser from displaying # the content. cat < EOD bugit_cmd_list | bugit_cgi_quote | sed "s|^\\([^:#]*\\)#\\([^:#]*\\):\\(.*\\)\$|\1#\2:\3|" cat < EOD bugit_cgi_footer } bugit_cgi_search () { local regexp="$1" qregexp="$(echo "$1" | bugit_cgi_quote)" bug_cgi_http_header bug_cgi_html_head "BuGit search - $qregexp" # FIXME: Add links to the corresponding issues! bugit_cmd_search "$regexp" | bugit_cgi_gitlog_to_html bugit_cgi_footer } bugit_cgi_main () { local reparg="$1" bug_cgi_http_header bug_cgi_html_head "BuGit main page" cat <
Show issue:

Show issues that match:

List all issues

EOD bugit_cgi_footer } bugit_cmd_cgi () { eval "$(bugit_make_optloop '' repository)" </dev/null 2>&1; then { reparg=${HTTP_HOST%%.*} [ -d "$reparg" ] } || { reparg=${SCRIPT_NAME%/*} case $reparg in */cgi-bin ) reparg=${reparg%/*}; esac reparg=${reparg##*/} } fi case $reparg in *[/.]* ) bugit_cgi_error "Invalid repository arg '$reparg'" ;; "" ) : ;; * ) [ -d "$reparg" ] || bugit_cgi_error "No directory at '$reparg'" cd "$reparg" ;; esac git rev-parse --git-dir >/dev/null 2>&1 || bugit_cgi_error "No repository at '$repository/$reparg'" if [ "" != "$(bugit_table_get cgi attachment)" ]; then bugit_cgi_show_attachment "$(bugit_table_get cgi issue)" \ "$(bugit_table_get cgi attachment)" elif [ "" != "$(bugit_table_get cgi issue)" ]; then bugit_cgi_show "$(bugit_table_get cgi issue)" elif [ "" != "$(bugit_table_get cgi regexp)" ]; then bugit_cgi_search "$(bugit_table_get cgi regexp)" elif [ "" != "$(bugit_table_get cgi list)" ]; then bugit_cgi_list "$reparg" elif [ "" = "$(bugit_table_keys cgi)" ] || [ " rep " = "$(bugit_table_keys cgi)" ]; then bugit_cgi_main "$reparg" else bugit_cgi_fail fi ;; * ) bugit_cgi_fail ;; esac } bugit_cgi_error () { bug_cgi_http_header bug_cgi_html_head "BuGit error" cat <$1 EOD bugit_cgi_footer exit 1 } ### Conversion from DebBugs ################################################### # Sizes for Emacs's debbugs: # original debbugs database = 100% # without leftover .report, .status, .BAK ...: 75% # zx-compressed = 10% # .git right after conversion = 25% # .git = 10% (after "git gc" and all). # numbers subdir = 2.5% # *.status is a relic from an older Debbugs format, can be ignored. # I think *.report as well. # # *.summary containds rfc822-style fields: # # Format-Version = "2" # ?Affects comma-list of ?packages?? # ?Blocks space-list of ?bug numbers?? # Blocked-By space-list of bug numbers => probably a user-defined set # Date large integer => date of the first commit # Done email address of closer => commit author of "tags/closed" # Fixed-Date ?large integer? => commit date of its file # Fixed-In space-list of versions => probably a user-defined set # Forwarded-To URL of other DB's bug number => ?? # Found-Date ?large integer? => commit date of its file # Found-In space-list of versions => probably a user-defined set # Merged-With space-list of bug numbers => ?? # Message-Id of the initial email => ?ID of bug report? # Owner email address of owner => owners # Package comma-list of package names => ?owners? # Severity word => severity level # Subject Title of the bug => NAME # Submitter email address => followers # Tags space separated list => tags # Unarchived date as a large integer => ?? # # Severities are: wishlist minor normal important serious grave critical # # Notes: "packages" can be mapped to "owners", i.e. treated as owners, # or to separate databases (since BuGit supports sharing bugs between several # databases). # # IOW for a good mapping, we need to add user-defined sets (for fixed, # found, and blocks), and we need to figure out how to handle archiving # and merging; oh! and forwarding. # - For merging, we could actually merge the respective branches (and mark # it in a new "merged-with" set, so we can re-merge the branches when # something is added to one of them). # - For archiving, we could move them to a separate database, but # "git push --all" will tend to re-add them all the time, so we need to # figure out a way to really *remove* bugit/ branches. # - For forwarding, we could use a new user-defined set, or make it built-in # for extra support (e.g. new messages could be sent upstream, maybe, # or the upstream bug could be queried so as to update the local bug). bugit_comma_split () { OLDIFS="$IFS"; IFS=", " set -- $1 IFS="$OLDIFS" echo "$@" } debbugs_rfc822_val () { local val="${1#*:}" while case $val in " "*) true;; *) false;; esac; do val=${val# }; done echo "$val" } debbugs_change_set () { set=$1; operation=$2; shift 2 case $operation in "=" | "+" ) if [ "=" = "$operation" ]; then rm -f "$set/"* fi bugit_mkdir "$set" for x; do touch "$set/$(bugit_to_filename "$x")" done ;; "-" ) for x; do rm -f "$set/$(bugit_to_filename "$x")" done ;; * ) invalid "Set operation '$operation' not among +/-/=" ;; esac git add -- "$set" } debbugs_summary () { # Process *.summary file. bugit_table_clear debbugs local version name while read line; do local key="${line%%:*}" local val="$(debbugs_rfc822_val "$line")" case $key in # FIXME: For format=2, emails and subject need to be decoded! Format-Version ) version=$val ;; Blocked-By ) debbugs_change_set "blocked-by" "=" $val ;; Date ) bugit_table_set debbugs date "$val" ;; Done ) bugit_table_set debbugs done "$val" ;; Fixed-Date ) bugit_table_set debbugs fixed_date "$val" ;; Fixed-In ) debbugs_change_set "fixed" "=" $val ;; Forwarded-To ) debbugs_change_set "forwarded" "=" "$val" ;; Found-Date ) bugit_table_set debbugs found_date "$val" ;; Found-In ) debbugs_change_set "found" "=" $val ;; Merged-With ) debbugs_change_set "merged" "=" $val ;; Message-Id ) bugit_table_set debbugs message_id "$val" ;; Owner ) debbugs_change_set "owners" "=" "$val" ;; Package ) debbugs_change_set "owners" "=" \ $(bugit_comma_split "$val") ;; Severity ) debbugs_change_set "severity" "=" \ $(debbugs_severity_level "$val") ;; Subject ) name=$(bugit_sanitize_name "$val") echo "$name" >name ;; Submitter ) debbugs_change_set "followers" "=" "$val" ;; Tags ) debbugs_change_set "tags" "=" $val ;; Unarchived ) bugit_table_set debbugs unarchived_date "$val" ;; * ) user_error "Unknown debbugs summary field '$key'" ;; esac [ "" != "$name" ] || echo "_" >name git add name done <"$1.summary" } debbugs_severity_level () { case $1 in wishlist) echo 0 ;; minor) echo 1 ;; normal) echo 2 ;; important) echo 3 ;; serious) echo 4 ;; grave) echo 5 ;; critical) echo 6 ;; *) user_error "Unknown severity '$1'" ;; esac } # *.report: holds the original bug-report prepended with # Received: (at submit) by debbugs.gnu.org; 17 Mar 2015 13:19:02 +0000 # From debbugs-submit-bounces@debbugs.gnu.org Tue Mar 17 09:19:02 2015 # # *.log: concatenation of all messages, each part separated with control # characters (always alone on a line). According to Debbugit/Log.pm: # =item incoming-recv # # ^G # [mail] # ^C # # C<[mail]> must start with /^Received: \(at \S+\) by \S+;/, and is # copied to the output. # # =item autocheck # # Auto-forwarded messages are recorded like this: # # ^A # [mail] # ^C # # C<[mail]> must contain /^X-Debian-Bugs(-\w+)?: This is an autoforward from # \S+/. The first line matching that is removed; all lines in the # message body that begin with 'X' will be copied to the output, minus # the 'X'. # # Nothing in debbugs actually generates this record type any more, but # it may still be in old .logs at some sites. # # =item recips # # ^B # [recip]^D[recip]^D[...] OR -t # ^E # [mail] # ^C # # Each [recip] is output after "Message sent"; C<-t> represents the same # sendmail option, indicating that the recipients are taken from the headers # of the message itself. # # =item html # # ^F # [html] # ^C # # [html] is copied unescaped to the output. The record immediately following # this one is considered "boring" and only shown in certain output modes. # # (This is a design flaw in the log format, since it makes it difficult to # change the HTML presentation later, or to present the data in an entirely # different format.) # # =back # # No other types of records are permitted, and the file must end with a ^C # line. # There are no ^A...^C in Emacs's debbugs database, it seems. # # ^B: "recips". followed by one line (an email address). # "-t" seems to be a special address # indicating that it's a message generated by Debbugs itself. # ^C: at the end of "go", "html", or "go-nox" # ^D: # ^E: "go" ? Followed by an email message (with lines potentially prefixed # with \030 (C-x?). # ^F: "html" ? Followed by embedded HTML code. # ^G: "incoming-recv" ? turned into "go" at the next line (which should # be "Received:..."; Followed by an email message. debbugs_log () { # Process *.log file. local msg while debbugs_log_1; do :; done <"$1.log" } debbugs_skip_to () { to=$1 while read line && [ "$to" != "$line" ]; do :; done } bugit_success_run () { "$@" || internal_error "Failure to run:" "$@" } debbugs_process_msg () { local author date # "read" strips spaces at start&end, so it can't be used to reliably find # the end of header. sed -ue '/^$/Q' >"hdr.$$" author=$(sed -ne '/^[Ff]rom:/{s/[^:]*: *//;p;q}' <"hdr.$$") date=$(sed -ne '/^[Dd]ate:/{s/[^:]*: *//;p;q}' <"hdr.$$") [ "" != "$author" ] || user_error "No From: in the message" [ "" != "$date" ] || user_error "No Date: in '$author's message (remains: $(wc))" rm -f "hdr.$$" # Git doesn't allow NUL bytes in messages. sed -ue '/^$/Q' | tr '\000' ' ' >"msg.$$" bugit_success_run \ git commit --allow-empty --allow-empty-message \ -F "msg.$$" --date="$date" \ --author="$(bugit_sanitize_author "$author" :)" rm -f "msg.$$" } debbugs_log_1 () { # Process next message from *.log file. read line && case $line in "") debbugs_skip_to "" if [ "skip" != "$1" ]; then debbugs_log_1 skip debbugs_log_1 fi ;; "") if [ "skip" != "$1" ]; then debbugs_process_msg else debbugs_skip_to "" fi ;; "") debbugs_skip_to "" if [ "skip" != "$1" ]; then debbugs_process_msg else debbugs_skip_to "" fi ;; "") internal_error "Unsupported ^A...^C entry in log file" ;; *) user_error "Unexpected line '$line' in log file, wc=$(wc)" ;; esac } debbugs_bug () { debbugs_summary "$1" git reset -- fixed tags/closed local found_date="$(bugit_table_get debbugs found_date)" # If there's no date, better put the "found" right into the first commit. [ "" = "$found_date" ] || git reset -- found debbugs_log "$1" # Process the entries in the "debbugs" table. [ "" = "$found_date" ] || { git add found bugit_commit "Set found to: $(cd found && echo *)" \ --date="$found_date" } local fixed_date="$(bugit_table_get debbugs found_date)" [ -d fixed ] && { git add fixed bugit_commit "Set fixed to: $(cd fixed && echo *)" \ --date="$fixed_date" } local doner="$(bugit_table_get debbugs done)" [ "" = "$doner" ] || { bugit_mkdir tags touch tags/closed git add tags/closed bugit_commit "Add to tags: closed" \ --author="$(bugit_sanitize_author "$doner" :)" } } bugit_cmd_debbugs () { eval "$(bugit_make_optloop '' from to)" <<'ENDDOC' Convert DebBugs database to BuGit format. FROM should be either the `db-h` or the `archive` subdirectory of a DebBugs database. TO should be any an empty target directory. ENDDOC from=$(cd "$from" 2>/dev/null && pwd) [ "" != "$from" ] || user_error "Can't find '$from'" bugit_mkdir "$to" cd $to mkdir master || user_error "'$to/master' already exists" (cd master; bugit_cmd_init) bugit_mkdir master/numbers git-new-workdir master issue echo "Converting Debbugs issues..." for f in "$from"/*/*.summary; do f=${f%.summary} bugit_debug "converting '$f'" number=${f##*/} case $number in *[!0-9]* ) user_error "Non-number file name '$f'"; esac id=$(bugit_generate_id) echo "$id" >"master/numbers/$number" (cd issue git reset --hard #bugit_assert_clean_p git checkout --orphan bugit/"$id" || user_error "Can't create branch 'bugit/$id'" git rm -rf . debbugs_bug "$f") done echo "Finalizing the conversion..." # Hoist "master" up to cwd. rm -rf issue; mv master/* master/.??* ./ && rmdir master # Commit the master branch. git add numbers bugit_commit "Assign numbers to issues: from '$from'" echo "Compacting the repository..." git reflog expire --all --expire=all git gc echo "Generating the names->id reverse map..." bugit_cmd_refresh_names echo "Converting Debbugs issues...done" } ### Main ###################################################################### if [ "$#" = 0 ]; then case "$0" in post-receive | */post-receive ) bugit_cmd_post_receive ;; bugit.cgi | */bugit.cgi ) bugit_cmd_cgi "/var/bugit" ;; *) invalid "BuGit usage:" esac else cmd=$1; shift function=$(bugit_function "$cmd") type "$function" >/dev/null || invalid "Unknown BuGit command '$cmd'" "$function" "$@" fi