Cómo iterar a través de todas las ramas de git usando el script bash

105

¿Cómo puedo iterar a través de todas las ramas locales en mi repositorio usando el script bash? Necesito iterar y verificar si hay alguna diferencia entre la rama y algunas ramas remotas. Ex

for branch in $(git branch); 
do
    git log --oneline $branch ^remotes/origin/master;
done

Necesito hacer algo como se indicó anteriormente, pero el problema al que me enfrento es $ (git branch) me da las carpetas dentro de la carpeta del repositorio junto con las ramas presentes en el repositorio.

¿Es esta la forma correcta de resolver este problema? ¿O hay otra forma de hacerlo?

Gracias

Arun P Johny
fuente
1
@pihentagy Esto se escribió antes de la pregunta vinculada.
kodaman
Por ejemplo: -for b in "$(git branch)"; do git branch -D $b; done
Abhi

Respuestas:

156

No debe usar git branch al escribir scripts. Git proporciona una interfaz de "plomería" que está diseñada explícitamente para su uso en secuencias de comandos (muchas implementaciones actuales e históricas de los comandos de Git normales (agregar, verificar, fusionar, etc.) utilizan esta misma interfaz).

El comando de plomería que desea es git for-each-ref :

git for-each-ref --shell \
  --format='git log --oneline %(refname) ^origin/master' \
  refs/heads/

Nota: No necesita el remotes/prefijo en la referencia remota a menos que tenga otras referencias que hagan origin/mastercoincidir varios lugares en la ruta de búsqueda del nombre de referencia (consulte "Un nombre de referencia simbólico. ..." en la sección Especificación de revisiones de git-rev-parse (1) ). Si usted está tratando de evitar la ambigüedad explicitamente, y luego ir con el nombre completo ref: refs/remotes/origin/master.

Obtendrá un resultado como este:

git log --oneline 'refs/heads/master' ^origin/master
git log --oneline 'refs/heads/other' ^origin/master
git log --oneline 'refs/heads/pu' ^origin/master

Puede canalizar esta salida a sh .

Si no le gusta la idea de generar el código de shell, puede renunciar a un poco de robustez * y hacer esto:

for branch in $(git for-each-ref --format='%(refname)' refs/heads/); do
    git log --oneline "$branch" ^origin/master
done

* Los nombres de referencia deben estar a salvo de la división de palabras del shell (consulte git-check-ref-format (1) ). Personalmente, me quedaría con la versión anterior (código de shell generado); Estoy más seguro de que nada inapropiado puede pasar con él.

Dado que especificó bash y admite matrices, puede mantener la seguridad y aún evitar generar las entrañas de su bucle:

branches=()
eval "$(git for-each-ref --shell --format='branches+=(%(refname))' refs/heads/)"
for branch in "${branches[@]}"; do
    # …
done

Podría hacer algo similar con $@si no está usando un shell que admita matrices ( set --para inicializar y set -- "$@" %(refname)agregar elementos).

Chris Johnsen
fuente
39
Seriamente. ¿No hay una forma más sencilla de hacer esto?
Jim Fell
4
Pero, ¿qué pasa si quiero usar una de las opciones de filtrado de git branch, como --merged, tendría que duplicar la lógica en git branch? Tiene que haber una mejor manera de hacer esto.
Thayne
3
Versión más sencilla:git for-each-ref refs/heads | cut -d/ -f3-
wid
4
@wid: O, simplemente,git for-each-ref refs/heads --format='%(refname)'
John Gietzen
5
@Thayne: esta pregunta es antigua, pero la gente de Git finalmente ha abordado el problema: for-each-refahora admite todos los selectores de rama como --mergedy, git branchy git tagahora se implementan en términos de git for-each-refsí mismos, al menos para los casos existentes en la lista. (La creación de nuevas ramas y etiquetas no es, y no debería ser, parte de for-each-ref.)
torek
50

Esto se debe a que git branchmarca la rama actual con un asterisco, por ejemplo:

$ git branch
* master
  mybranch
$ 

por lo que se $(git branch)expande a eg * master mybranch, y luego se *expande a la lista de archivos en el directorio actual.

No veo una opción obvia para no imprimir el asterisco en primer lugar; pero podrías cortarlo:

$(git branch | cut -c 3-)
Matthew Slattery
fuente
4
Si rodea entre comillas dobles, puede evitar que bash expanda el asterisco, aunque aún querrá eliminarlo de la salida. Una forma más sólida de eliminar un asterisco de cualquier punto sería $(git branch | sed -e s/\\*//g).
Nick
2
agradable, me gusta mucho tu 3-solución.
Andrei-Niculae Petre
1
Versión sed un poco más simple:$(git branch | sed 's/^..//')
jdg
5
versión tr un poco más simple:$(git branch | tr -d " *")
ccpizza
13

El bash incorporado mapfile, está construido para esto

todas las ramas de git: git branch --all --format='%(refname:short)'

todas las sucursales locales de git: git branch --format='%(refname:short)'

todas las ramas de git remotas: git branch --remotes --format='%(refname:short)'

iterar a través de todas las ramas de git: mapfile -t -C my_callback -c 1 < <( get_branches )

ejemplo:

my_callback () {
  INDEX=${1}
  BRANCH=${2}
  echo "${INDEX} ${BRANCH}"
}
get_branches () {
  git branch --all --format='%(refname:short)'
}
# mapfile -t -C my_callback -c 1 BRANCHES < <( get_branches ) # if you want the branches that were sent to mapfile in a new array as well
# echo "${BRANCHES[@]}"
mapfile -t -C my_callback -c 1 < <( get_branches )

para la situación específica del PO:

#!/usr/bin/env bash


_map () {
  ARRAY=${1?}
  CALLBACK=${2?}
  mapfile -t -C "${CALLBACK}" -c 1 <<< "${ARRAY[@]}"
}


get_history_differences () {
  REF1=${1?}
  REF2=${2?}
  shift
  shift
  git log --oneline "${REF1}" ^"${REF2}" "${@}"
}


has_different_history () {
  REF1=${1?}
  REF2=${2?}
  HIST_DIFF=$( get_history_differences "${REF1}" "${REF2}" )
  return $( test -n "${HIST_DIFF}" )
}


print_different_branches () {
  read -r -a ARGS <<< "${@}"
  LOCAL=${ARGS[-1]?}
  for REMOTE in "${SOME_REMOTE_BRANCHES[@]}"; do
    if has_different_history "${LOCAL}" "${REMOTE}"; then
      # { echo; echo; get_history_differences "${LOCAL}" "${REMOTE}" --color=always; } # show differences
      echo local branch "${LOCAL}" is different than remote branch "${REMOTE}";
    fi
  done
}


get_local_branches () {
  git branch --format='%(refname:short)'
}


get_different_branches () {
  _map "$( get_local_branches )" print_different_branches
}


# read -r -a SOME_REMOTE_BRANCHES <<< "${@}" # use this instead for command line input
declare -a SOME_REMOTE_BRANCHES
SOME_REMOTE_BRANCHES=( origin/master remotes/origin/another-branch another-remote/another-interesting-branch )
DIFFERENT_BRANCHES=$( get_different_branches )

echo "${DIFFERENT_BRANCHES}"

fuente: enumera todas las ramas locales de git sin un asterisco

Andrew Miller
fuente
5

Repetiré como por ejemplo:

for BRANCH in `git branch --list|sed 's/\*//g'`;
  do 
    git checkout $BRANCH
    git fetch
    git branch --set-upstream-to=origin/$BRANCH $BRANCH
  done
git checkout master;
Djory Krache
fuente
4

Sugeriría $(git branch|grep -o "[0-9A-Za-z]\+")si sus sucursales locales se nombran solo por dígitos, az y / o letras de AZ

Xin Guo
fuente
4

La respuesta aceptada es correcta y realmente debería ser el enfoque utilizado, pero resolver el problema en bash es un gran ejercicio para comprender cómo funcionan los shells. El truco para hacer esto usando bash sin realizar manipulación de texto adicional, es asegurarse de que la salida de la rama git nunca se expanda como parte de un comando que ejecutará el shell. Esto evita que el asterisco se expanda en la expansión del nombre de archivo (paso 8) de la expansión del shell (consulte http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_04.html )

Use bash while build con un comando de lectura para cortar la salida de la rama git en líneas. El '*' se leerá como un carácter literal. Utilice una declaración de caso para que coincida, prestando especial atención a los patrones de coincidencia.

git branch | while read line ; do                                                                                                        
    case $line in
        \*\ *) branch=${line#\*\ } ;;  # match the current branch
        *) branch=$line ;;             # match all the other branches
    esac
    git log --oneline $branch ^remotes/origin/master
done

Los asteriscos tanto en la construcción del caso bash como en la sustitución de parámetros deben escaparse con barras invertidas para evitar que el shell los interprete como caracteres de coincidencia de patrones. Los espacios también se escapan (para evitar la tokenización) porque literalmente está haciendo coincidir '*'.

BitByteDog
fuente
4

Opción más fácil de recordar en mi opinión:

git branch | grep "[^* ]+" -Eo

Salida:

bamboo
develop
master

La opción -o de Grep (--only-matching) restringe la salida a solo las partes coincidentes de la entrada.

Dado que ni el espacio ni * son válidos en los nombres de las ramas de Git, esto devuelve la lista de ramas sin los caracteres adicionales.

Editar: si está en el estado de 'cabeza separada' , deberá filtrar la entrada actual:

git branch --list | grep -v "HEAD detached" | grep "[^* ]+" -oE

staafl
fuente
3

Lo que terminé haciendo, aplicado a su pregunta (e inspirado por la mención de ccpizza tr):

git branch | tr -d ' *' | while IFS='' read -r line; do git log --oneline "$line" ^remotes/origin/master; done

(Yo uso muchos bucles while. Mientras que para cosas particulares definitivamente querrás usar un nombre de variable puntiagudo ["rama", por ejemplo], la mayoría de las veces solo me preocupa hacer algo con cada línea de entrada. Usando 'línea' aquí en lugar de 'rama' es un guiño a la reutilización / memoria muscular / eficiencia).

Jan Kyu Peblik
fuente
1

A partir de la respuesta de @ finn (¡gracias!), Lo siguiente le permitirá iterar sobre las ramas sin crear un script de shell intermedio. Es lo suficientemente robusto, siempre que no haya nuevas líneas en el nombre de la rama :)

git for-each-ref --format='%(refname)' refs/heads  | while read x ; do echo === $x === ; done

El bucle while se ejecuta en una subcapa, lo que generalmente está bien a menos que esté configurando variables de shell a las que desea acceder en el shell actual. En ese caso, usa la sustitución de procesos para revertir la tubería:

while read x ; do echo === $x === ; done < <( git for-each-ref --format='%(refname)' refs/heads )
Chris Cogdon
fuente
1

Si estás en este estado:

git branch -a

* master

  remotes/origin/HEAD -> origin/master

  remotes/origin/branch1

  remotes/origin/branch2

  remotes/origin/branch3

  remotes/origin/master

Y ejecuta este código:

git branch -a | grep remotes/origin/*

for BRANCH in `git branch -a | grep remotes/origin/*` ;

do
    A="$(cut -d'/' -f3 <<<"$BRANCH")"
    echo $A

done        

Obtendrás este resultado:

branch1

branch2

branch3

master
Wijith Bandara
fuente
Esto me parece una solución menos compleja +1
Abhi
Esto también funcionó para mí:for b in "$(git branch)"; do git branch -D $b; done
Abhi
1

Mantenlo simple

La forma sencilla de obtener el nombre de la rama en un bucle mediante el script bash.

#!/bin/bash

for branch in $(git for-each-ref --format='%(refname)' refs/heads/); do
    echo "${branch/'refs/heads/'/''}" 
done

Salida:

master
other
Googlian
fuente
1

La respuesta de Googlian, pero sin usar para

git for-each-ref --format='%(refname:lstrip=-1)' refs/heads/
Maestro Yoda
fuente
1
Esto no funciona para nombres de ramas con espacios de nombres, como en las ramas que tienen una barra inclinada. Esto significa que las ramas creadas por dependdabot, que se parecen a "dependdabot / npm_and_yarn / typescript-3.9.5", aparecerán en su lugar como "typescript-3.9.5".
Ecbrodie
1
for branch in "$(git for-each-ref --format='%(refname:short)' refs/heads)"; do
    ...
done

Esto usa comandos de plomería de git , que están diseñados para secuencias de comandos. También es simple y estándar.

Referencia: finalización de Bash de Git

aude
fuente