1#!/usr/bin/env bash
2# Module Manager
3#
4#   Copyright 2009 Colin Mollenhour
5#
6#   Licensed under the Apache License, Version 2.0 (the "License");
7#   you may not use this file except in compliance with the License.
8#   You may obtain a copy of the License at
9#
10#       http://www.apache.org/licenses/LICENSE-2.0
11#
12#   Unless required by applicable law or agreed to in writing, software
13#   distributed under the License is distributed on an "AS IS" BASIS,
14#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15#   See the License for the specific language governing permissions and
16#   limitations under the License.
17#
18#   System Requirements:
19#    - bash
20#    - filesystem and project or web server must support symlinks
21#    - The following common utilities must be locatable in your $PATH
22#       grep (POSIX), find, ln, cp, basename, dirname, readlink
23
24version="1.12"
25script=${0##*/}
26usage="\
27Module Manager (v$version)
28
29Global Commands:
30  $script <command> [<options>]
31------------------------------
32  init [basedir]     initialize the pwd (or [basedir]) as the modman deploy root
33  list [--absolute]  list all valid modules that are currently checked out
34  status             show VCS 'status' command for all modules
35  incoming           show new VCS remote changesets for all VCS-based modules
36  update-all         update all modules that are currently checked out
37  deploy-all         deploy all modules (no VCS interaction)
38  repair             rebuild all modman-created symlinks (no updates performed)
39  clean              clean up broken symlinks (run this after deleting a module)
40  automodman         loops through module directory:
41                     - asks to include files & directories in new modman file
42                     - does not create symlinks
43  --help             display this help message
44  --tutorial         show a brief tutorial on using modman
45  --version          display the modman script's version
46  [--force]          overwrite existing files, ignore untrusted cert warnings
47  [--no-local]       skip processing of modman.local files
48  [--no-clean]       skip cleaning of broken symlinks
49  [--no-shell]       skip processing @shell lines
50
51Module Commands:
52  $script <command> [<module>] [<options>] [[--] <VCS options>]
53------------------------------
54  checkout <src>     checkout a new modman-compatible module using subversion
55  clone <src>        checkout a new modman-compatible module using git clone
56  hgclone <src>      checkout a new modman-compatible module using hg clone
57  link <path>        link to a module that already exists on the local filesystem
58  update             update module using the appropriate VCS
59  deploy             deploy an existing module (VCS not required or used)
60  undeploy           remove an existing module without deleting the files
61  skip               prevent a module from deploying with deploy-all
62  unskip             re-enable a module to be deployed with deploy-all
63  remove             remove a module (DELETES MODULE FILES)
64  [--force]          overwrite existing files, ignore untrusted cert warnings
65  [--no-local]       skip processing of modman.local files
66  [--no-clean]       skip cleaning of broken symlinks
67  [--no-shell]       skip processing @shell lines
68  [--basedir <path>] on checkout/clone, specifies a base for module deployment
69  [--copy]           deploy by copying files instead of symlinks
70  [<VCS options>]    specify additional parameters to VCS checkout/clone
71"
72tutorial="\
73Deploying a modman module:
74------------------------------
75The 'checkout' and 'clone' commands are used to checkout new modules from a
76repository (subversion and git, respectively). These commands support additional
77arguments which will be passed to the 'svn checkout' and 'git clone' commands.
78This allows you to for example specify a --username (svn) or a --branch (git).
79See 'svn help checkout' and 'git help clone' for a full list of options.
80Both svn:externals and git submodules will be automatically included.
81
82To link to modules that already exist on the local file system you can use
83the 'link' command. The 'update' command will be supported the same as if the
84module had been created using the checkout or clone command. For example:
85
86    $ modman link ~/projects/My_Module
87    $ modman update My_Module
88
89By default, if a module being checked out contains symlink mappings that
90conflict with existing files, an error will be thrown. Use --force to cause
91any existing files or directories that conflict to be removed. Be careful!
92
93Writing modman modules:
94------------------------------
95Each module should contain a file named \"modman\" which defines which files
96go where relative to the directory where modman was initialized.
97
98==== Start example modman file ====
99   # Comments are supported, begin a line with a hash
100   code                   app/code/local/My/Module/
101   design                 app/design/frontend/default/default/mymodule/
102
103   # Source and destination can have different names.
104   en_US.csv              app/locale/en_US/My_Module.csv
105
106   # Destination file name is not required if the same as the source.
107   My_Module.xml          app/etc/modules/
108
109   # Leave out the destination entirely to assume the same path as the source.
110   lib/Somelib
111
112   # Bash extended glob patterns supported.
113   skin/css/*             skin/frontend/base/default/css/
114   skin/js/*              skin/frontend/base/default/js/
115
116   # Import another modman module
117   @import                modules/Fooman_Speedster
118
119   # Execute a command on the shell
120   @shell                 rm -rf \$PROJECT/var/cache/*
121==== End example modman file ====
122
123Globbing:
124------------------------------
125Bash globbing in the modman file is supported. The result is that each file or
126directory that matches the globbing pattern will get its own symlink in the
127specified location when the module is deployed.
128
129Importing modules:
130------------------------------
131One modman file can import another modman module using @import and the path to
132the imported module's root relative to the current module's root. Imported
133modules deploy to the same path as checked out modules so @import can be used
134to include common modules that are just stored in a subdirectory, or are
135included via svn:externals or git submodules. Example:
136    > svn propget svn:externals .
137    ^/modules/Fooman_Speedster  modules/Fooman_Speedster
138In this example, modules/Fooman_Speedster would contain it's own modman file
139and could therefore be imported by any other module or checked out by itself.
140
141Shell commands:
142------------------------------
143Actions to be taken any time one of the checkout, clone, update, deploy,
144update-all or repair commands are used can be defined with the @shell
145directive. The rest of the line after @shell will be piped to a new bash
146shell with the working directory being the module's root. The following
147environment variables will be made available:
148  PROJECT    The path of the project base dir (where modman was initialized)
149  MODULE     The current module's path
150
151Standalone mode:
152------------------------------
153The modman script can be used without a VCS by placing the module directory in
154the proper location and running \"modman deploy <module>\". The root of the
155module must be located at <project_root>/.modman/<module_name>/ and it must
156contain a modman file.
157
158Shortcut:
159------------------------------
160Modman can be run without the module name if the current directory's realpath
161is within a module's path. E.g. \"modman update\" or \"modman deploy\".
162
163Local 'modman' file:
164------------------------------
165You can create a modman file named \"modman.local\" which will also get applied
166like the standard modman file. The intended purpose of this file is to allow you
167to specify additional directives while excluding them from version control. The
168\"--no-local\" command option will prevent these files from being processed.
169
170Custom 'clean' script:
171------------------------------
172On some systems the dead link search can be brutally slow. You can create a
173custom clean script at .modman/.clean which will override the default clean
174script. Here is an example of a custom clean script that ignores the \"media\"
175and \"var\" directories:
176
177    echo Cleaning dead links in \$PWD
178    find -L . -mount \\( -path ./media -o -path ./var \\) -prune -o -type l -exec rm {} \;
179
180Author:
181------------------------------
182Colin Mollenhour
183http://colin.mollenhour.com/
184colin@mollenhour.com
185"
186
187# Action is first argument
188action=$1; shift
189
190# Fix GNU inconsistencies with Mac
191case $OSTYPE in
192  darwin*|*bsd*)
193    stat_type="bsd_stat"
194    readlink_missing="echo" # readlink -m not supported
195    ;;
196  *)
197    stat_type="stat -c %F"
198    readlink_missing="readlink -m"
199    ;;
200esac
201bsd_stat ()
202{
203  case "$(stat -f %T "$1")" in
204    "*") echo file;;
205    "@") echo symlink;;
206    "/") echo directory;;
207    *) echo unknown_type;;
208  esac
209}
210pager=${PAGER:-$(which pager &> /dev/null)}
211if [ -z "$pager" ]; then
212  pager=less
213fi
214
215###########################
216# Handle "init" command, simply create .modman directory
217if [ "$action" = "init" ]; then
218  basedir=$1
219  [[ -n "$basedir" && ! -d "$basedir" ]] && { echo "$basedir is not a directory."; exit 1; }
220  mkdir .modman || { echo "Could not create .modman directory" && exit 1; }
221  if [ -n "$basedir" ]; then
222    echo "$basedir" > .modman/.basedir
223    basedir="with basedir at $basedir"
224  fi
225  echo "Initialized Module Manager at $(pwd) $basedir"
226  exit 0
227###########################
228# Handle "--help" command
229elif [ "$action" = "--help" -o "$action" = "" ]; then
230  echo -e "$usage"; exit 0
231###########################
232# Handle "--tutorial" command
233elif [ "$action" = "--tutorial" ]; then
234  echo -e "$tutorial" | $pager; exit 0
235###########################
236# Handle "--version" command
237elif [ "$action" = "--version" ]; then
238  echo "Module Manager version: $version"; exit 0
239fi
240
241
242#############################################################
243# Echo in bold font if stdout is a terminal
244ISTTY=0; if [ -t 1 ]; then ISTTY=1; fi
245bold () { if [ $ISTTY -eq 1 ]; then tput bold; fi; }
246unbold () { if [ $ISTTY -eq 1 ]; then tput sgr0; fi; }
247echo_b ()
248{
249  if [ "$1" = "-e" ]; then
250    echo -e "$(bold)$2$(unbold)"
251  else
252    echo "$(bold)$1$(unbold)"
253  fi
254}
255
256#############################################################
257# Colors in terminal
258yellow () { if [ $ISTTY -eq 1 ]; then tput setaf 3; fi; }
259red () { if [ $ISTTY -eq 1 ]; then tput setaf 1; fi; }
260
261#############################################################
262# Handling warning messages
263warning ()
264{
265  echo -e "$(bold)$(yellow)WARNING:$(unbold) "$1
266}
267
268#############################################################
269# Handling Error messages
270error ()
271{
272  echo -e "$(bold)$(red)ERROR:$(unbold) "$1
273}
274
275#############################################################
276# Check for existence of a module directory and modman file
277require_wc ()
278{
279  if ! [ -d "$mm/$1" ]; then
280    error "$1 has not been checked out."; return 1
281  fi
282  if ! [ -r "$mm/$1/modman" ]; then
283    error "$1 does not contain a \"modman\" module description file."; return 1
284  fi
285  return 0
286}
287
288###############################################################
289# Removes dead symlinks
290remove_dead_links ()
291{
292  if [ $NOCLEAN -eq 1 ]; then
293    return 0
294  fi
295
296  # Support use of a custom clean script
297  if [ -r "$mm/.clean" ]; then
298    ( cd "$root"; $SHELL "$mm/.clean" )
299  else
300    # Use -exec rm instead of -delete to avoid bug in Darwin. -Thanks Vinai!
301    ( cd "$root"; find -L . -mount -type l -exec rm {} \; )
302  fi
303  return $?
304}
305
306###############################################################
307# Removes all .basedir files from submodules
308remove_basedirs ()
309{
310  local module=$1
311  find "$mm/$module" -name .basedir | grep -FZv "$mm/$module/.basedir" | xargs -0 rm -f
312}
313
314###############################################################
315# Reads the base directory for a module
316# Return value is blank, or relative path ending with /
317get_basedir ()
318{
319  local module_dir=$1
320  local basedir=''
321  if [ -r "$module_dir/.basedir" ]; then
322    basedir=$(cat "$module_dir/.basedir" | grep -v '^#')
323    if [ -n "$basedir" ]; then
324      basedir=${basedir%%/}/
325    fi
326  fi
327  echo -n "$basedir"
328}
329
330###############################################################
331# Writes a file, setting the base directory for a module
332set_basedir ()
333{
334  local module_dir=$1
335  local basedir=${2##/}
336  basedir=${basedir%%/}
337  if [ -n "$basedir" ]; then
338    echo -e "# This file was created by modman. Module base directory:\n$basedir" \
339      > "$module_dir/.basedir"
340    if ! [ $? ]; then
341      error "Could not write to file: $module_dir/.basedir."
342      return 1
343    fi
344  fi
345  return 0
346}
347
348get_skipped ()
349{
350  local module=$1
351  if [ -f "$root/.modman-skip" ]; then
352    for line in $(grep -v -e '^#' "$root/.modman-skip"); do
353      if [ $line == $module ]; then
354        return 0
355      fi
356    done
357  fi
358  return 1
359}
360
361set_skipped ()
362{
363  local module=$1
364  local skip=$2
365  local SKIP_FILE="$root/.modman-skip"
366
367  if ! [ -f "$SKIP_FILE" ]; then
368    echo "# This file was created by modman. The following modules won't be deployed by deploy-all." \
369      > "$root/.modman-skip"
370    if ! [ $? ]; then
371      error "Could not write to file: $SKIP_FILE"
372      return 1
373    fi
374  fi
375
376  if [ "$skip" = 1 ]; then
377    if get_skipped $module; then
378      echo "Module $module already skipped."
379      exit 1
380    else
381      echo $module >> "$SKIP_FILE"
382      echo "Module $module added to skip list."
383    fi
384  else
385    grep -v "^$module$" "$SKIP_FILE" > "$SKIP_FILE.tmp"; mv "$SKIP_FILE.tmp" "$SKIP_FILE"
386    echo "Module $module removed from skip list."
387  fi
388
389  return 0
390}
391
392get_abs_filename() {
393  if [ -d "$(dirname "$1")" ]; then
394    echo "$(cd "$(dirname "$1")/$(dirname "$(readlink "$1")")" && pwd)/$(basename "$1")"
395  fi
396}
397
398remove_module_links ()
399{
400  echo "Removing links for module $module."
401  local module_dir="$mm/$module"
402  for line in $(find $root -type l); do
403    if [[ $(get_abs_filename "$line") =~ ^"$module_dir".* ]]; then
404      rm "$line"
405    fi
406  done
407
408  return 0
409}
410
411################################################################################
412# Reads a modman file and does the following:
413#   Creates the symlinks as described
414#   Imports external modman files (@import)
415#   Runs shell commands (@shell)
416apply_modman_file ()
417{
418  local module=$1
419  local module_dir=$(dirname "$module")
420  local basedir=$(get_basedir "$module_dir")
421  local relpath=${module:$((${#mmroot}+1))}
422
423  # Use argument if module doesn't have a .basedir file
424  if [ -z "$basedir" ]; then
425    basedir=$2
426  fi
427
428  # while loop should not read from stdin or else @shell scripts cannot get stdin
429  IFS=$'\r\n'
430  for line in $(grep -v -e '^#' -e '^\s*$' "$module"); do
431    IFS=$' \t\n'
432
433    # Split <target> <real>
434    read target real <<< $line
435
436    # Assume target == real if only one path is given
437    if [ -z "$real" ]; then
438        real="$target"
439    fi
440
441    # Sanity check for empty data
442    if [ -z "$target" -o -z "$real" ]; then
443      error "Invalid input in modman file ($relpath):\n $line"
444      return 1
445    fi
446
447    # Import other module definitions (e.g. git submodules, svn:externals, etc..)
448    if [ "$target" = "@import" ]; then
449      # check if base defined, create and save base to .basedir file
450      read import_path import_base <<< $real
451
452      import=$module_dir/${import_path%/}/modman
453      if ! [ -r "$import" ]; then
454        relimport=${import:$((${#mmroot}+1))}
455        error "modman file not found ($relimport):\n $line"
456        return 1
457      fi
458
459      if [ -z "$import_base" ]; then
460        import_base=${basedir%%/}
461      else
462        import_base=${import_base##/}
463        import_base=${basedir}${import_base%%/}
464        if ! [ -d "$root/$import_base" ]; then
465          if ! mkdir -p "$root/$import_base"; then
466            error "Could not create import base directory: $import_base"
467            return 1
468          fi
469          echo "Created import base directory: $import_base"
470        fi
471        if ! set_basedir "$module_dir/$import_path" "$import_base"; then
472          return 1
473        fi
474      fi
475
476      apply_modman_file "$import" "$import_base/" || return 1
477      continue
478    fi
479
480    # Run commands on the shell!
481    # temporary file is workaround so that script can receive stdin
482    if [ "$target" = "@shell" ]; then
483      [ $NOSHELL -eq 0 ] || continue
484      cd "$module_dir"
485      export PROJECT=$root/${basedir%%/}
486      export MODULE=$module_dir
487      shell_tmp=$(mktemp "$mm/.tmp.XXXXXXX")
488      echo "($real)" > "$shell_tmp"
489      source "$shell_tmp"
490      rm -f "$shell_tmp"
491      continue
492    fi
493
494    # Create symlink to target
495    local src=$module_dir/$target
496    local dest=$root/${basedir}${real%/}
497    dest=${dest/\/\//\/} # Replace // with /
498    dest=${dest%/} # Strip trailing /
499
500    # Handle globbing (extended globbing enabled)
501    shopt -s extglob
502    if ! [ -e "$src" ] && [ $(ls $src 2> /dev/null | wc -l) -gt 0 ]; then
503      for _src in $src; do
504        apply_path "$_src" "$dest/${_src##*/}" "$target" "${real%/}/${_src##*/}" "$line" || return 1
505      done
506      continue
507    fi # end Handle globbing
508
509    # Handle aliases that do not exist
510    if ! [ -e "$src" ];
511    then
512      warning "Target does not exist ($relpath):\n $line"
513      continue
514    fi
515
516    # Allow destination to be a dir when src is a file
517    if [ -f "$src" ] && [ -d "$dest" -o "/" = ${real: -1} ]; then
518      dest="$dest/$(basename "$src")"
519    fi
520
521    apply_path "$src" "$dest" "$target" "$real" "$line" || return 1
522  done
523
524  return 0
525}
526
527###########################################################################
528# Creates a symlink or copies a file (with lots of error-checking)
529apply_path ()
530{
531  local src="$1"; local dest="$2"; local target="$3"; local real="$4"; local line="$5"
532
533  # Make symlinks relative
534  if [ $COPY -eq 0 ]; then
535    local realpath=$($readlink_missing "${dest%/*}"); local commonpath=""
536    if [ "${dest%/*}" == "${realpath}" ]; then
537      # Use modman root as common path if destination is not itself a symlinked path
538      commonpath="${mmroot%/}"
539    else
540      # Search for longest common path as symlink target
541      for ((i=0; i<${#dest}; i++)); do
542        if [[ "${dest:i:1}" != "${realpath:i:1}" ]]; then
543          commonpath="${dest:0:i}"
544          commonpath="${commonpath%/*}"
545          break
546        fi
547      done
548    fi
549    # Replace destination (less common path) with ../*
550    if [ "$commonpath" != "" ]; then
551      local reldest="${dest#$commonpath/}"
552      if [ "$reldest" != "${reldest%/*}" ]; then
553        reldest=$(IFS=/; for d in ${reldest%/*}; do echo -n '../'; done)
554      else
555        reldest=""
556      fi
557      src="${reldest}${src#$commonpath/}"
558    fi
559  fi
560
561  # Handle cases where files already exist at the destination or link does not match expected destination
562  if [ -e "$dest" ];
563  then
564    if ! [ -L "$dest" ] && [ $FORCE -eq 0 ]; then
565      echo_b "CONFLICT: $($stat_type "$dest") already exists and is not a symlink:"
566      echo_b " $line"
567      echo   "(Run with $(bold)--force$(unbold) to force removal of existing files and directories.)"
568      return 1
569    elif ! [ -L "$dest" ] || [ "$src" != "$(readlink "$dest")" ]; then
570      warning "Removing conflicting $($stat_type "$dest"): $dest"
571      rm -rf "$dest" || return 1
572    fi
573  fi
574
575  # Create links if they do not already exist
576  if ! [ -e "$dest" ];
577  then
578    # Delete conflicting symlinks that are broken
579    if [ -L "$dest" ]; then
580      rm -f "$dest"
581    fi
582    # Create parent directories
583    if ! mkdir -p "${dest%/*}"; then
584      error "Unable to create parent directory (${dest%/*}):\n $line"
585      return 1
586    fi
587    # Symlink or copy
588    success=0
589    if [ $COPY -eq 1 ]; then
590      verb='copy'
591      cp -R "$src" "$dest" && success=1
592    else
593      verb='create symlink'
594      ln -s "$src" "$dest" && success=1
595    fi
596    if [ $success -eq 1 ]; then
597      printf " Applied: %-30s  %s\n" "$target" "$real"
598    else
599      error "Unable to $verb ($dest):\n $line"
600      return 1
601    fi
602  fi
603  return 0
604}
605###########################################################################
606# Automatically create a modman file
607run_automodman () #"$module" "$mm" "$wc_dir" "$wc_desc"
608{
609  # returns 0 on success 1 on error
610  amodule=$1
611  amm=$2
612  awc_dir=$3
613  awc_desc=$4
614
615  #echo "building modman file for $amodule"
616  #echo "in base directory $amm"
617  #echo "in working copy $awc_dir"
618  #echo "modman file will be or is $awc_desc"
619
620  #does the modman file exist?
621  #yes
622  if [ -r "$awc_desc" ]; then
623    echo "$awc_desc file already exists"
624    return 0 #exit without error
625
626  #no
627  else
628    #touch modman
629    echo "creating modman file for $amodule"
630    touch $awc_desc
631  fi
632  #loop through files
633  declare -a incpaths
634
635  for f in $(find $awc_dir \( ! -iname 'modman' ! -regex "$awc_dir.*/\..*" \)) #ignore hidden files and modman
636    do
637      #strip path down
638      alngth=${#awc_dir} #length of working directory to remove from path
639      alngth=$((alngth+1)) #increase by one to account for the fact we added a slash
640      pathname=${f:$alngth}
641
642      #first f tends to be empty because of our stripping the path down so test for it -n is default operator
643      if [ "$pathname" ]; then
644
645        prevmatch=0
646        #todo: possibly there is a neater way to do this without a loop??
647        for incPath in "${incpaths[@]}" #loop through incpaths
648        do
649          if [[ "$pathname" != "${pathname/$incPath/}" ]]; then
650            prevmatch=1
651          fi
652        done
653
654        #if substring then do recurse
655        if [[ $prevmatch -eq 0 ]]; then #make sure we have not previously matched
656
657          #pre-existing path: yes | no?
658          if grep -rq "^[^#].*\s$pathname$" "${amm}/"*"/modman" #target is what is important so find end line not beginning line
659          then #pre-existing path: yes
660            echo "..."
661            echo "A path was found that exists in below modman description file" #error
662            grep -r --color=always "\s$pathname$" "${amm}/"*"/modman"
663            echo "It is therefore presently illegal for it to be included in this module."
664            echo "..."
665            if [[ -d $f ]]; then
666                    incpaths+=("$pathname")
667                fi
668            read -p "press enter to continue..." goonthen
669          else #pre-existing path: no
670
671            read -p "include '$pathname': [N][y]" include #include path: yes | no?
672            if [[ "$include" == "" ]]; then
673                 include="N"
674            fi
675
676            if [ "$include" == "y" -o "$include" == "Y" ]; then #include path: yes
677
678              echo "..."
679              echo "You answered y:yes modman will include the path"
680              echo "..."
681
682              #write path to modman
683              echo "$pathname  $pathname" >> $awc_desc #substring ${var:start}
684              #was it a directory or file
685              if [[ -d $f ]]; then
686                  incpaths+=("$pathname")
687              fi
688
689            elif [ "$include" == "n" -o "$include" == "N" ]; then #include path: no
690              echo "You answered N:no, modman is not including the path"
691            else
692              echo "Warning: you answered $include which is invalid, modman is not including path"
693            fi
694
695          fi # test other modules for pre-existing path
696        fi # test prvious match
697      fi #continue
698    done #no more paths
699    #return success
700    return 0
701}
702
703###########################################################################
704# Get git remote tracking branch or an empty string
705get_tracking_branch ()
706{
707  local tracking_branch=$(git rev-parse --symbolic-full-name --abbrev-ref @{u} 2> /dev/null)
708  if [ -n "$tracking_branch" -a "$tracking_branch" != "@{u}" ]; then
709    echo $tracking_branch
710  fi
711}
712
713################################
714# Find the .modman directory and store parent path in $root
715mm_not_found="Module Manager directory not found.\nRun \"$script init\" in the root of the project with which you would like to use Module Manager."
716_pwd=$(pwd -P)
717root=$_pwd
718while ! [ -d "$root/.modman" ]; do
719  if [ "$root" = "/" ]; then echo -e $mm_not_found && exit 1; fi
720  cd .. || { error "Could not traverse up from $root\n$mm_not_found" && exit 1; }
721  root=$(pwd)
722done
723
724mmroot=$root          # parent of .modman directory
725mm=$root/.modman      # path to .modman
726
727# Allow a different root to be specified as root for deploying modules, applies to all modules
728newroot=$(get_basedir "$mm")
729if [ -n "$newroot" ]; then
730  cd "$mmroot/$newroot" || {
731    error "Could not change to basedir specified in .basedir file: $newroot"
732    exit 1
733  }
734  root=$(pwd)
735fi
736
737# Check for common option overrides
738FORCE=0               # --force option off by default
739NOLOCAL=0             # --no-local option off by default
740NOCLEAN=0             # --no-clean off by default
741NOSHELL=0             # --no-shell option off by default
742COPY=0                # --copy option off by default
743basedir=''
744while true; do
745  case "$1" in
746    --force)       FORCE=1; shift ;;
747    --no-local)    NOLOCAL=1; shift ;;
748    --no-clean)    NOCLEAN=1; shift ;;
749    --no-shell)    NOSHELL=1; shift ;;
750    --copy)        COPY=1; shift ;;
751    --basedir)
752      shift; basedir="$1"; shift
753      if ! [ -n "$basedir" -a -d "$root/$basedir" ]; then
754        echo "Specified --basedir does not exist: $basedir"; exit 1
755      fi
756      ;;
757    *) break ;;
758  esac
759done
760
761###############################
762# Handle "list" command
763if [ "$action" = "list" ]; then
764  prefix=''
765  if [ "$1" = "--absolute" ]; then shift; prefix="$mm/"; fi
766  if [ -n "$1" ]; then echo "Too many arguments to list command."; exit 1; fi
767  for module in $(ls -1 "$mm"); do
768    if [ -d "$mm/$module" -a -e "$mm/$module/modman" ]; then
769      echo "${prefix}$module"
770    fi
771  done
772  exit 0
773
774###############################
775# Handle "status" command
776elif [ "$action" = "status" ]; then
777  if [ -n "$1" ]; then echo "Too many arguments to status command."; exit 1; fi
778  for module in $(ls -1 "$mm"); do
779    if [ -d "$mm/$module" -a -e "$mm/$module/modman" ]; then
780      cd "$mm/$module"
781      echo_b "-- $module --"
782      if [ -d "$mm/$module/.git" ]; then
783        git status
784      elif [ -d "$mm/$module/.svn" ]; then
785        svn status
786      elif [ -d "$mm/$module/.hg" ]; then
787        hg status
788      else
789        echo "Not a git, hg or svn repository."
790      fi
791      echo
792    fi
793  done
794  exit 0
795
796###############################
797# Handle "incoming" command
798elif [ "$action" = "incoming" ]; then
799  if [ -n "$1" ]; then echo "Too many arguments to incoming command."; exit 1; fi
800  tmpfile=$(mktemp "$mm/.incoming.XXXX")
801  for module in $(ls -1 "$mm"); do
802    if [ -d "$mm/$module" -a -e "$mm/$module/modman" ]; then
803      cd "$mm/$module"
804      echo_b "-- $module --" >> $tmpfile
805      if [ -d "$mm/$module/.git" ]; then
806        tracking_branch=$(get_tracking_branch)
807        if [ -z $tracking_branch ]; then
808          echo "Could not resolve remote tracking branch for $module module." >> $tmpfile
809        else
810          echo "Fetching updates for $module..."
811          git fetch && git --no-pager log --color ..origin/master >> $tmpfile
812        fi
813      elif [ -d "$mm/$module/.svn" ]; then
814        svn st --show-updates >> $tmpfile
815      elif [ -d "$mm/$module/.hg" ]; then
816        hg incoming >> $tmpfile
817      else
818        echo "Not a git, hg or svn repository." >> $tmpfile
819      fi
820      echo >> $tmpfile
821    fi
822  done
823  less -fFR $tmpfile
824  rm $tmpfile
825  exit 0
826
827###############################
828# Handle "deploy-all" command
829elif [ "$action" = "deploy-all" ]; then
830  if [ -n "$1" ]; then echo "Too many arguments to deploy-all command."; exit 1; fi
831  remove_dead_links
832  errors=0
833  for module in $(ls -1 "$mm"); do
834    test -d "$mm/$module" && require_wc "$module" || continue;
835    if get_skipped "$module"; then
836      echo "Skipping module $module due to .modman-skip file."
837      continue
838    fi
839    echo "Deploying $module to $root"
840    if apply_modman_file "$mm/$module/modman"; then
841      echo -e "Deployment of '$module' complete.\n"
842      if [ $NOLOCAL -eq 0 -a -r "$mm/$module/modman.local" ]; then
843        apply_modman_file "$mm/$module/modman.local" && echo "Applied local modman file for $module"
844      fi
845    else
846      error "Error occurred while deploying '$module'.\n"
847      errors=$((errors+1))
848    fi
849  done
850  echo "Deployed all modules with $errors errors."
851  exit 0
852
853###############################
854# Handle "update-all" command
855#   Updates source code, removes dead links and then deploys modules
856elif [ "$action" = "update-all" ]; then
857  if [ -n "$1" ]; then echo "Too many arguments to update-all command."; exit 1; fi
858  update_errors=0
859  updated=''
860
861  # Fetch first in case an origin is not responding or slow
862  for module in $(ls -1 "$mm"); do
863    test -d "$mm/$module" && require_wc "$module" || continue;
864    cd "$mm/$module"
865    success=1
866    if [ -d .git ] && [ "$(git remote)" != "" ]; then
867      echo "Fetching changes for $module"
868      success=0
869      if [ $FORCE -eq 1 ]; then
870        if git status -s | grep -vq '??'; then
871          echo "Cannot do --force update, module has uncommitted changes."
872          exit 1
873        else
874          git fetch --force && success=1
875        fi
876      else
877        git fetch && success=1
878      fi
879    fi
880    if [ $success -eq 1 ]; then
881      updated="${updated}${module}\n"
882    else
883      echo_b -e "Failed to fetch updates for $module\n"
884      update_errors=$((update_errors+1))
885    fi
886  done
887
888  # Then update using merge
889  for module in $(echo -en "$updated"); do
890    test -d "$mm/$module" && require_wc "$module" || continue;
891    cd "$mm/$module"
892    success=0
893    if [ -d .svn ]; then
894      echo "Updating $module"
895      if [ $FORCE -eq 1 ]; then
896        svn update --force --non-interactive --trust-server-cert && success=1
897      else
898        svn update && success=1
899      fi
900    elif [ -d .git ] && [ "$(git remote)" != "" ]; then
901      tracking_branch=$(get_tracking_branch)
902      echo "Updating $module"
903      if [ -z $tracking_branch ]; then
904        echo "Could not resolve remote tracking branch, code will not be updated."
905      elif [ $FORCE -eq 1 ]; then
906        git reset --hard $tracking_branch && git submodule update --init --recursive && success=1
907      else
908        git merge $tracking_branch && git submodule update --init --recursive && success=1
909      fi
910    elif [ -d .hg ]; then
911      echo "Updating $module"
912      hg pull && hg update && success=1
913    else
914      success=1
915    fi
916    echo
917    if [ $success -ne 1 ]; then
918      error "Error occurred while updating $module\n"
919      update_errors=$((update_errors+1))
920    fi
921  done
922  remove_dead_links
923  deploy_errors=0
924  for module in $(ls -1 "$mm"); do
925    test -d "$mm/$module" && require_wc "$module" || continue;
926    if apply_modman_file "$mm/$module/modman"; then
927      echo -e "Deployment of '$module' complete.\n"
928      if [ $NOLOCAL -eq 0 -a -r "$mm/$module/modman.local" ]; then
929        apply_modman_file "$mm/$module/modman.local" && echo "Applied local modman file for $module"
930      fi
931    else
932      error "Error occurred while deploying '$module'.\n"
933      deploy_errors=$((deploy_errors+1))
934    fi
935  done
936  echo "Updated all modules with $update_errors update errors and $deploy_errors deploy errors."
937  exit 0
938
939###########################
940# Handle "repair" command
941elif [ "$action" = "repair" ]; then
942  echo "Repairing links, do not interrupt."
943  mv "$mm" "$mm-repairing" || { error "Could not temporarily rename .modman directory."; exit 1; }
944  remove_dead_links
945  mv "$mm-repairing" "$mm" || { error "Could not restore .modman directory."; exit 1; }
946  for module in $(ls -1 "$mm"); do
947    test -d "$mm/$module" && require_wc "$module" || continue;
948    remove_basedirs "$module" &&
949    apply_modman_file "$mm/$module/modman" &&
950    echo -e "Repaired $module.\n"
951    if [ $NOLOCAL -eq 0 -a -r "$mm/$module/modman.local" ]; then
952      apply_modman_file "$mm/$module/modman.local" && echo "Applied local modman file for $module"
953    fi
954  done
955  exit 0
956
957###########################
958# Handle "clean" command
959elif [ "$action" = "clean" ]; then
960  echo "Cleaning broken links."
961  NOCLEAN=0
962  remove_dead_links
963  exit 0
964fi
965
966#############################################
967# Handle all other module-specific commands
968#############################################
969
970REGEX_ACTION='^(update|deploy|undeploy|skip|unskip|checkout|clone|hgclone|link|remove|automodman)$'
971REGEX_NEW_MODULE='^(checkout|clone|hgclone|link)$'
972REGEX_BAD_MODULE="($REGEX_ACTION| )"
973REGEX_MODULE='^[a-zA-Z0-9_-]+$'
974
975if ! [[ "$action" =~ $REGEX_ACTION ]]; then
976  echo "Invalid action specified: $action"
977  exit 1
978fi
979
980module=''
981src=''
982
983# If valid module is specified on command line
984if [ -z "$module" -a -n "$1" -a -d "$mm/$1" ] || [[ "$1" =~ $REGEX_MODULE ]]; then
985  module=$1; shift
986fi
987
988# If module name is not given
989if [ -z "$module" ]; then
990  # Extract from end of next argument assuming it is the repo location
991  if [[ "$action" =~ $REGEX_NEW_MODULE ]]; then
992    if [ $# -eq 1 -o "${2:0:1}" = "-" ]; then
993      module=${1%.git}             # strip .git if specified
994      module=${module//:/\/}       # replace : with / for basename in case of git SSH
995      module=$(basename "$module") # get the end-most part of the repo url
996    fi
997
998  # Discover if modman is run from within a module directory
999  else
1000    cd "$_pwd"
1001    while [ $(dirname "$mm") != "$(pwd)" ] && [ "$(pwd)" != "/" ]; do
1002      modpath=$(pwd)
1003      if [ $(dirname "$modpath") = "$mm" ]; then
1004        module=$(basename "$modpath")
1005        break
1006      fi
1007      cd ..
1008    done
1009  fi
1010fi
1011
1012# Module must be next argument
1013if [ -z "$module" -a -n "$1" ]; then
1014  module=$1; shift
1015fi
1016if [ -z "$module" ]; then
1017  echo "Not enough arguments (no module specified)"
1018  exit 1
1019fi
1020
1021# Get optional args again (allow them to come after the module name)
1022while true; do
1023  case "$1" in
1024    --force)       FORCE=1; shift ;;
1025    --no-local)    NOLOCAL=1; shift ;;
1026    --no-clean)    NOCLEAN=1; shift ;;
1027    --no-shell)    NOSHELL=1; shift ;;
1028    --copy)        COPY=1; shift ;;
1029    --basedir)
1030      shift; basedir="$1"; shift
1031      if ! [ -n "$basedir" -a -d "$root/$basedir" ]; then
1032        echo "Specified --basedir does not exist: $basedir"; exit 1
1033      fi
1034      ;;
1035    *)
1036      break
1037  esac
1038done
1039
1040cd "$_pwd";               # restore old root
1041wc_dir=$mm/$module        # working copy directory for module
1042wc_desc=$wc_dir/modman    # path to modman structure descriptor file
1043
1044case "$action" in
1045
1046  automodman)
1047    run_automodman "$module" "$mm" "$wc_dir" "$wc_desc"
1048  ;;
1049  update)
1050    require_wc "$module" || exit 1
1051    cd "$wc_dir"
1052    success=0
1053    if [ -d .svn ]; then
1054      if [ $FORCE -eq 1 ]; then
1055        svn update --force --non-interactive --trust-server-cert && success=1
1056      else
1057        svn update && success=1
1058      fi
1059    elif [ -d .git ]; then
1060      if [ $FORCE -eq 1 ]; then
1061        tracking_branch=$(git rev-parse --symbolic-full-name --abbrev-ref @{u})
1062        [[ -n $tracking_branch ]] || { echo "Could not resolve remote tracking branch."; exit 1; }
1063        git fetch --force && git reset --hard $tracking_branch && git submodule update --init --recursive && success=1
1064      else
1065        git pull && git submodule update --init --recursive && success=1
1066      fi
1067    elif [ -d .hg ]; then
1068        hg pull && hg update && success=1
1069    fi
1070    [ $success -eq 1 ] || { echo_b "Failed to update working copy of '$module'."; exit 1; }
1071
1072    remove_dead_links
1073    apply_modman_file "$wc_desc" && echo "Update of $module complete."
1074    if [ $NOLOCAL -eq 0 -a -r "$wc_desc.local" ]; then
1075      apply_modman_file "$wc_desc.local" && echo "Applied local modman file for $module"
1076    fi
1077    ;;
1078
1079  checkout|clone|hgclone|link)
1080    FORCE=1
1081    cd "$mm"
1082    if [[ "$module" =~ $REGEX_BAD_MODULE ]]; then
1083      echo "You cannot $action a module with a name matching $REGEX_BAD_MODULE."; exit 1
1084    fi
1085    if [ -d "$wc_dir" ]; then
1086      echo "A module named '$module' has already been checked out."; exit 1
1087    fi
1088
1089    if [ -z "$src" ]; then
1090      src="$1"; shift
1091    fi
1092    if [ -z "$src" -o "$src" = "--" ]; then
1093      echo "You must specify a source for the '$action' command."; exit 1
1094    fi
1095    if [ "$1" = "--" ]; then shift; fi
1096
1097    success=0
1098    verb=''
1099    if [ "$action" = "checkout" ]; then
1100      verb='checked out'
1101      svn checkout "$src" $@ "$module" && success=1
1102    elif [ "$action" = "clone" ]; then
1103      verb='cloned'
1104      git clone --recursive "$src" $@ "$module" && success=1
1105    elif [ "$action" = "hgclone" ]; then
1106      verb='cloned'
1107      hg clone "$src" $@ "$module" && success=1
1108    elif [ "$action" = "link" ]; then
1109      verb='linked'
1110      cd "$mmroot"
1111      if ! [ -d "$src" ]; then
1112        echo "The path specified does not exist or is not a directory."
1113        echo "The module path must either be an absolute path, or relative to $mmroot"
1114        exit 1
1115      fi
1116      if [ "${src:0:1}" != "/" ]; then
1117        src="../$src"
1118      fi
1119      ln -s "$src" ".modman/$module" && success=1
1120      cd "$mm"
1121    fi
1122
1123    if
1124      [ $success -eq 1 ] &&
1125      require_wc "$module" && cd "$wc_dir" &&
1126      set_basedir "$wc_dir" "$basedir" &&
1127      apply_modman_file "$wc_desc"
1128    then
1129      if [ -n "$basedir" ]; then
1130        using_basedir=" using base directory '$basedir'"
1131      fi
1132      echo "Successfully $verb new module '$module'$using_basedir"
1133    else
1134      if [ -d "$wc_dir" ]; then rm -rf "$wc_dir"; fi
1135      error "trying to $action new module '$module', operation cancelled."
1136      exit 1
1137    fi
1138    ;;
1139
1140  deploy)
1141    require_wc "$module" || exit 1
1142    apply_modman_file "$wc_desc" && echo "$module has been deployed under $root"
1143    if [ $NOLOCAL -eq 0 -a -r "$wc_desc.local" ]; then
1144      apply_modman_file "$wc_desc.local" && echo "Applied local modman file for $module"
1145    fi
1146    ;;
1147
1148  undeploy)
1149    require_wc "$module" || exit 1
1150    remove_module_links "$module" || exit 1
1151    ;;
1152
1153  skip)
1154    set_skipped "$module" 1 || exit 1
1155    ;;
1156
1157  unskip)
1158    set_skipped "$module" 0 || exit 1
1159    ;;
1160
1161  remove)
1162    require_wc "$module" || exit 1
1163    rm -rf "$wc_dir" && remove_dead_links && echo "$module has been removed"
1164    ;;
1165
1166  *)
1167    echo -e "$usage"
1168    echo_b "Invalid action: $action"
1169    exit 1
1170
1171esac
1172
1173