initial commit
[git-wtree.git] / git-wtree.sh
1 #!/bin/sh
2
3 ### git_wtree_new <name:branch name> [dir:<dir name>]
4 ### git_wtree_drop <name:branch name>
5 ### git_wtree_{cd, pushd} <name:branch name>
6 ### git_wtree_root
7 ### git_wtree_ls
8
9 # TODO: use 'git check-ref-format --branch' to check branch's name validness
10
11 ### GIT_WTREE_ALIAS_ENABLED=true
12
13 ### For debugging purposes:
14 ### GIT_WTREE_DRY_RUN=true
15 ### GIT_WTREE_DEBUG=true
16
17 _git_wtree_last_error_dir=$(mktemp --suffix=git.wtree. -d)
18 _git_wtree_last_error_msg=${_git_wtree_last_error_dir}/msg
19 _git_wtree_last_error_out=${_git_wtree_last_error_dir}/out
20 trap "test -d ${_git_wtree_last_error_dir} && rm -rf '${_git_wtree_last_error_dir}'" EXIT QUIT
21
22 ### Execute `git' subcommand with passed arguments
23 ### $1     : routine description
24 ### $2     : git subcommand, e.g.: worktree
25 ### ${n,}  : git subcomand's arguments
26 _git_wtree_exec() {
27     _git_wtree_set_last_error "$1 FAILED"
28     shift
29
30     cmd="git $*"
31     if [ -n "${GIT_WTREE_DRY_RUN}" ]; then
32         echo "[git-wtree:DRY-RUN]: ${cmd}"
33         return 0
34     fi
35
36     [ -n "${GIT_WTREE_DEBUG}" ] && echo -e "git:{\n"
37     ${cmd}
38     rc=$?
39     [ -n "${GIT_WTREE_DEBUG}" ] && echo -e "\n}:git"
40
41     return $rc
42 }
43
44 ### Return key's value from arguments
45 ### $1    : arg's name
46 ### ${2,n}: argv to search in
47 ### NOTE: for the sake of simplicity no long values are supported
48 _git_wtree_arg() {
49     arg_name=$1
50     shift
51
52     while [ ! $# -eq 0 ]; do
53         case "$1" in
54             --${arg_name}) echo -n $2 && break ;;
55             *) shift ;;
56         esac
57     done
58 }
59
60 _git_wtree_show_last_error() {
61     echo "[git-wtree:ERROR] $(cat ${_git_wtree_last_error_msg})" >&2
62     [ -s "${_git_wtree_last_error_out}" ] && echo "[git-wtree:ERROR] $(cat ${_git_wtree_last_error_out})" >&2
63     true
64 }
65
66 _git_wtree_set_last_error() {
67     echo "$*" > ${_git_wtree_last_error_msg}
68 }
69
70 _git_wtree_worktree_root() {
71     worktree_root=$(git config worktree.root 2>${_git_wtree_last_error_out} || echo -n)
72     [ -n "${worktree_root}" -a -d "${worktree_root}" ] && echo -n "${worktree_root}" && return 0
73     _git_wtree_set_last_error "Non-defined or not accessible worktree root '${worktree_root}'. 'worktree.root' in .git/config should point to an existing dir."
74     return 1
75 }
76
77 _git_wtree_arg_dir_name() {
78     dir_name=$(_git_wtree_arg dir $*)
79     _git_wtree_set_last_error "Missing directory name. Shall be specified with --dir argument"
80     [ -n "${dir_name}" ] && echo -n "${dir_name}" && return 0
81     return 1
82 }
83
84 _git_wtree_arg_branch_name() {
85     branch_name=$(_git_wtree_arg branch $*)
86
87     _git_wtree_set_last_error "Missing branch name. Shall be specified with either --branch or --name argument"
88     [ -z "${branch_name}" ] && branch_name=$(_git_wtree_arg name $*)
89     [ -n "${branch_name}" ] && echo -n "${branch_name}" && return 0
90     return 1
91 }
92
93 _git_wtree_dir_by_branch_name() {
94     branch_name="$1"
95     candidates_amount=$(git worktree list 2>${_git_wtree_last_error_out} | grep "${branch_name}" | wc -l)
96
97     ### TODO: won't work for names with a common prefix
98     _git_wtree_set_last_error "No exact branch '${branch_name}' is available but ${candidates_amount} candidates"
99     [ 1 -eq "${candidates_amount}" ] || return 1
100
101     _git_wtree_set_last_error "No worktree dir is found for branch '${branch_name}'"
102     dir_name=$(git worktree list 2>/dev/null | grep "${branch_name}" | cut -d ' ' -f 1)
103     [ -n "${dir_name}" ] || return 1
104
105     _git_wtree_set_last_error "No worktree dir '${dir_name}' is available for branch '${branch_name}'"
106     [ -d "${dir_name}" ] || return 1
107
108     echo -n "${dir_name}"
109     return 0
110 }
111
112 ###
113 ### Locate a corresponding directory of specified branch.
114 ### Arguments:
115 ### --<name|branch> <branch_name>
116 ###
117 git_wtree_cmd_locate() {
118     current_top_level=$(git rev-parse --show-toplevel 2>${_git_wtree_last_error_out} || echo -n)
119     _git_wtree_set_last_error "'$(pwd)' is not a git directory"
120     [ -z "${current_top_level}" ] && _git_wtree_show_last_error && return 1
121
122     branch_name=$(_git_wtree_arg_branch_name $*)
123     [ -z "${branch_name}" ] && _git_wtree_show_last_error && return 1
124
125     branch_dir_name=$(_git_wtree_dir_by_branch_name ${branch_name})
126     [ -z "${branch_dir_name}" ] && _git_wtree_show_last_error && return 1
127
128     echo -n "${branch_dir_name}"
129     return 0
130 }
131
132 ####
133 #### Same as `git_wtree_cmd_locate`. Suppresses an error output. A corresponding exit code is preserved.
134 ####
135 git_wtree_cmd_locate_noerror() {
136     branch_dir_name=$(git_wtree_cmd_locate $* 2>/dev/null)
137     [ 0 -eq $? ] || return 1
138
139     echo -n "${branch_dir_name}"
140     return 0;
141 }
142
143 ###
144 ### Drop worktree's directory
145 ### Arguments:
146 ### --<name|branch> <branch_name>
147 ###   Should be specified in a relative way
148 ###
149 git_wtree_cmd_drop() {
150     branch_name=$(_git_wtree_arg_branch_name $*)
151     [ -z "${branch_name}" ] && _git_wtree_show_last_error && return 1
152
153     branch_dir_name=$(_git_wtree_dir_by_branch_name ${branch_name})
154     [ -z "${branch_dir_name}" ] && _git_wtree_show_last_error && return 1
155
156     # echo "- '${branch_name}' to be dropped from '${branch_dir_name}'"
157
158     cmd="git worktree remove ${branch_dir_name}"
159     _git_wtree_exec                                 \
160         "Drop directory of branch '${branch_name}'" \
161         worktree remove ${branch_dir_name}
162     [ 0 -ne $? ] && _git_wtree_show_last_error && return 1
163     return 0
164 }
165
166 ###
167 ### Create a new worktree branch
168 ### Arguments:
169 ### --<name|branch> <branch_name>
170 ### --dir <worktree_directory name>
171 ###   Should be specified in a relative way
172 ###
173 git_wtree_cmd_new() {
174     branch_name=$(_git_wtree_arg_branch_name $*)
175     [ -z "${branch_name}" ] && _git_wtree_show_last_error && return 1
176     branch_dir_name=$(_git_wtree_arg_dir_name $*)
177     [ -z "${branch_dir_name}" ] && _git_wtree_show_last_error && return 1
178     parent_branch_dir_name=$(_git_wtree_worktree_root)
179     [ -z "${parent_branch_dir_name}" ] && _git_wtree_show_last_error && return 1
180     
181     branch_dir_name="${parent_branch_dir_name}/${branch_dir_name}"
182     _git_wtree_set_last_error "'${branch_dir_name}' already exists"
183     [ -e "${branch_dir_name}" ] && _git_wtree_show_last_error && return 1
184
185     # echo "- '${branch_name}' to be created in '${branch_dir_name}'"
186    
187     _git_wtree_exec                                              \
188         "Create branch '${branch_name}' in '${branch_dir_name}'" \
189          worktree add -b ${branch_name} ${branch_dir_name} $(_git_wtree_arg commit $*)
190     [ 0 -ne $? ] && _git_wtree_show_last_error && return 1
191     return 0
192 }
193
194 ###
195 ### List all available worktrees
196 ### Arguments: none
197 ###
198 git_wtree_cmd_ls() {
199     _git_wtree_exec                \
200         "List available worktrees" \
201         worktree list | awk -F'[][]' '{print $2}'
202     [ 0 -ne $? ] && _git_wtree_show_last_error && return
203 }
204
205 ###
206 ### Helper to change a current directory to a worktree's one using a specified change dir command. Nothing is executed in a case of any error.
207 ### Arguments:
208 ### $1 - change dir command. E.g.: cd, pushd etc.
209 ### $2 - branch name
210 ###
211 git_wtree_cmd_tool_chdir() {
212     chdir_cmd=$1
213     shift
214     branch_dir_name=$(git_wtree_cmd_locate_noerror --name $1)
215     [ 0 -eq $? ] && eval "${chdir_cmd} ${branch_dir_name}"
216 }
217
218 ###
219 ### Helper to change a current directory to a worktree's one using 'cd'
220 ### Arguments:
221 ### $1 - branch name
222 ###
223 git_wtree_cmd_tool_cd() {
224     git_wtree_cmd_tool_chdir cd $1
225 }
226
227 ###
228 ### Helper to change a current directory to a worktree's one using 'pushd'
229 ### Arguments:
230 ### $1 - branch name
231 ###
232 git_wtree_cmd_tool_pushd() {
233     git_wtree_cmd_tool_chdir pushd $1
234 }
235
236 git_wtree() {
237     [ 0 -eq $# ] && _git_wtree_set_last_error "Command expected: new, drop, locate" && _git_wtree_show_last_error && return
238
239     cmd=git_wtree_cmd_$1
240     shift
241
242     ${cmd} $*
243     return $?
244 }
245
246 if [ -n "${GIT_WTREE_ALIAS_ENABLED}" ]; then
247     alias git.wtree=git_wtree
248     alias git.wtree:ls=git_wtree_cmd_ls
249     alias git.wtree:new=git_wtree_cmd_new
250     alias git.wtree:drop=git_wtree_cmd_drop
251     alias git.wtree:cd=git_wtree_cmd_tool_cd
252     alias git.wtree:pushd=git_wtree_cmd_tool_pushd
253 fi
254