-
Notifications
You must be signed in to change notification settings - Fork 0
/
offbranch
executable file
·208 lines (180 loc) · 5.4 KB
/
offbranch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/bin/bash
# initialize gettext
export TEXTDOMAIN='git'
# Refuse to run outside any repository.
if ! git rev-parse 2> /dev/null
then
gettext -s "Not a git repository" >&2
exit 9
fi
# Compose resources in a temporary directory.
readonly tmpdir="$(mktemp -d --tmpdir offbranch.XXXXXXXXXX)"
trap "rm -r '$tmpdir'" EXIT
export GIT_INDEX_FILE="$tmpdir/index"
# Parse arguments and compose new revision.
add() {
if [ ! -r "$1" ]
then
printf "$(gettext "pathspec '%s' did not match any files")\n" "$1" >&2
exit 8
elif [ -n "$2" ]
then
if [ -d "$1" ]
then
find $1/ -type f | while read file
do
add "$file" "${file/#"$1"/"$2"}"
done
elif [ -h "$1" ]
then
git update-index --add --cacheinfo 120000,$(
readlink -n "$1" | git hash-object --path "$2" -w --stdin
),"$2"
else
git update-index --add --cacheinfo $(
stat -c '%a' "$1"),$(
git hash-object --path "$2" -w "$1"
),"$2"
fi
fi
}
readonly toplevel="$(git rev-parse --show-toplevel)"
add-argument() {
filename="${1%${separator:=:}*}"
add "$filename" "${1##*$separator}"
if [[ "$(realpath -s "$filename")" =~ ^"$toplevel" ]]
then
messages+=("$filename")
fi
}
add-list() {
while read -rd "$1"
do
add-argument "$REPLY"
done < "$OPTARG"
}
while [ $OPTIND -le $# ]
do
if getopts 'hs:b:n:m:l:z:' argument
then
case $argument in
h)
echo "usage: $0 [options] <files>"
echo 'Commit files on top of a branch not checked out.'
echo ' -h show this help text'
echo ' -s <separator> separator between real file name and path in repository'
echo ' -b <branch> branch to commit to'
echo ' -n <number> commit messages to propose'
echo ' -m <message> fix message'
echo ' -l <file> read file list from file'
echo ' -z <file> read zero delimited file list'
exit ;;
s) separator="$OPTARG" ;;
b) branch="refs/heads/$OPTARG" ;;
n) number="$OPTARG" ;;
m) text="$OPTARG" ;;
l) add-list $'\n' ;;
z) add-list $'' ;;
\?) exit 8 ;;
esac
else
add-argument "${!OPTIND}"
let OPTIND++
fi
done
# Propose commit message, if necessary.
# This separator tells a commit message apart from unprefixed comments.
# It is taken from git commit -v.
snip="------------------------ >8 ------------------------"
# Output a $snip line, properly explained and commented.
snipline() {
printf "$(gettext $(
# Enclosing them in command substitutions is a weird
# but compact way to ignore newlines as well as indents.
)$'Please enter the commit message for your changes. Lines starting\n'$(
)$'with \'%c\' will be ignored, and an empty message aborts the commit.\n')\n" \
"$(echo | git stripspace -c)"
# Passing an empty line through stripspace generates the comment character,
# followed by a space. Command substiution will strip the latter.
echo "$snip"
gettext $'Do not touch the line above.\nEverything below will be removed.'
}
# Find how the given <tree-ish> differs from the index.
# If no argument is given, the constant empty tree is assumed.
readonly diff="$tmpdir/diff"
diffinfo() {
git diff-index -p --cached --no-color --exit-code \
"${1:-4b825dc642cb6eb9a060e54bf8d69288fbee4904}" \
2> /dev/null | tee "$diff"
return ${PIPESTATUS[0]}
}
readonly message="$tmpdir/COMMIT_EDITMSG"
{ if [ -n "$text" ]
then
echo "$text"
else
git log --format="%B%n$(printf '%0.s-' {1..72})" \
-n ${number:-1} -- "${messages[@]}" | head -n -2
fi
echo
snipline | git stripspace -c
diffinfo "${branch:=refs/heads/offbranch}"
} > "$message"
case $? in
0) # Nothing changed, nothing to commit.
exit 3 ;;
1) # Changes to commit
parent="$branch"
echo "updating ${branch#refs/heads/}"
;;
128) # Branch does not yet exist.
diffinfo >> "$message"
echo "creating ${branch#refs/heads/}"
ret=2
;;
esac
# Assort file names for processed edited commit message.
readonly clean="$tmpdir/COMMIT_MSG"
readonly newdiff="$tmpdir/editdiff"
# Patch leaving alone the working tree, displaying only errors.
patch-index() {
git apply --cached "$@" > /dev/null
}
# Patches look from the root. Go there.
cd "$(git rev-parse --show-toplevel)"
# This loop is not indented purposely. It just hooks the place to
# return to when the edited diff patch does not apply. The program
# always exits before reaching the done at the very end of this program.
while :
do
# Edit commit message.
eval "$(git var GIT_EDITOR)" '"$message"'
# Clean everything below snip line, comments and whitespace.
sed "/$snip/,\$d" "$message" | git stripspace -s > "$clean"
# Reject empty messages.
if [ ! -s "$clean" ]
then
gettext $'Aborting commit due to empty commit message.\n'
exit 1
fi
# Detach and apply changed diff.
sed "0,/$snip/d" "$message" > "$newdiff"
if [ -s "$newdiff" ] && ! cmp -s "$diff" "$newdiff"
then
git apply --cached -R "$diff" > /dev/null
if ! git apply --cached --recount "$newdiff" > /dev/null
then
git apply --cached "$diff" > /dev/null
read -sn1
continue # Start editing commit anew.
fi
fi
# Commit.
git update-ref "$branch" $(\
git commit-tree $(git write-tree) \
-F "$clean" ${parent+-p "$parent"} ) \
${parent+"$parent"} \
-m "commit (${ret:+"initial, "}off branch): $(head -n1 "$clean")"
exit $ret
# Close the while : hook. This is never reached.
done