#!/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]*\\);|\\'\$(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
*""*";"* )
str=$(bugit_echo "$str" |
sed -e 's/\\/\\\\/g' \
-e 's/%/%%/g' \
-e "s|\\([0-9a-fA-F]\\{2\\}\\);|\\\u00\\1|g" \
-e "s|\\([0-9a-fA-F]\\{4\\}\\);|\\\u\\1|g" \
-e "s|\\([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|^\([^:]*\):\(.*\)|
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 "
"
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|^\\([^:#]*\\)#\\([^:#]*\\):\\(.*\\)\$|
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