Como iterar em todos os branches do git usando o script bash

105

Como posso iterar por todos os branches locais em meu repositório usando o script bash. Preciso iterar e verificar se há alguma diferença entre o branch e alguns branches remotos. Ex

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

Eu preciso fazer algo como o fornecido acima, mas o problema que estou enfrentando é $ (git branch) me dá as pastas dentro da pasta do repositório junto com os branches presentes no repositório.

Esta é a maneira correta de resolver o problema? Ou existe outra maneira de fazer isso?

Obrigado

Arun P Johny
fonte
1
@pihentagy Isso foi escrito antes da pergunta vinculada.
kodaman 01 de
Por exemplo: -for b in "$(git branch)"; do git branch -D $b; done
Abhi

Respostas:

156

Você não deve usar o branch git ao escrever scripts. Git fornece uma interface de "encanamento" que é explicitamente projetada para uso em scripts (muitas implementações atuais e históricas de comandos normais do Git (adicionar, verificar, mesclar, etc.) usam essa mesma interface).

O comando de encanamento que você deseja é git for-each-ref :

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

Nota: Você não precisa do remotes/prefixo no ref remoto, a menos que você tenha outros refs que causem origin/mastera correspondência de vários lugares no caminho de pesquisa de nome de ref (consulte “Um nome de ref simbólico.…” Na seção Especificando revisões de git-rev-parse (1) ). Se você está tentando evitar explictly ambigüidade, em seguida, ir com o nome ref completa: refs/remotes/origin/master.

Você obterá uma saída como esta:

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

Você pode canalizar essa saída para sh .

Se você não gosta da ideia de gerar o código do shell, pode abrir mão de um pouco de robustez * e fazer o seguinte:

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

* Nomes de ref devem ser protegidos da divisão de palavras do shell (consulte git-check-ref-format (1) ). Pessoalmente, eu ficaria com a versão anterior (código de shell gerado); Estou mais confiante de que nada de impróprio pode acontecer com isso.

Como você especificou o bash e ele oferece suporte a matrizes, você pode manter a segurança e ainda evitar a geração do seu loop:

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

Você poderia fazer algo semelhante $@se não estiver usando um shell que suporte matrizes ( set --para inicializar e set -- "$@" %(refname)adicionar elementos).

Chris Johnsen
fonte
39
Seriamente. Não existe uma maneira mais simples de fazer isso?
Jim Fell
4
Mas e se eu quiser usar uma das opções de filtragem do git branch, como --merged, eu teria que duplicar a lógica no git branch? Tem que haver uma maneira melhor de fazer isso.
Thayne de
3
Versão mais simples:git for-each-ref refs/heads | cut -d/ -f3-
wid
4
@wid: Ou, simplesmente,git for-each-ref refs/heads --format='%(refname)'
John Gietzen
5
@Thayne: esta questão é antiga, mas o pessoal do Git finalmente resolveu o problema: for-each-refagora suporta todos os seletores de branch como --mergede git branche git tagagora estão realmente implementados em termos de git for-each-refsi mesmos , pelo menos para os casos existentes na lista. (Criar novos branches e tags não é, e não deveria ser, parte for-each-ref.)
torek de
50

Isso ocorre porque git branchmarca a ramificação atual com um asterisco, por exemplo:

$ git branch
* master
  mybranch
$ 

então se $(git branch)expande para * master mybranch, por exemplo , e então se *expande para a lista de arquivos no diretório atual.

Não vejo uma opção óbvia para não imprimir o asterisco em primeiro lugar; mas você pode cortá-lo:

$(git branch | cut -c 3-)
Matthew Slattery
fonte
4
Se você colocar aspas duplas, poderá impedir que o bash expanda o asterisco - embora ainda queira removê-lo da saída. Uma maneira mais robusta de remover um asterisco de qualquer ponto seria $(git branch | sed -e s/\\*//g).
Nick,
2
legal, eu realmente gosto da sua 3-solução.
Andrei-Niculae Petre
1
Versão sed um pouco mais simples:$(git branch | sed 's/^..//')
jdg
5
versão tr um pouco mais simples:$(git branch | tr -d " *")
ccpizza de
13

O bash embutido mapfile, é construído para isso

todos os branches do git: git branch --all --format='%(refname:short)'

todos os branches locais do git: git branch --format='%(refname:short)'

todos os branches remotos do git: git branch --remotes --format='%(refname:short)'

iterar por meio de todos os branches do git: mapfile -t -C my_callback -c 1 < <( get_branches )

exemplo:

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 a situação específica do OP:

#!/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}"

source: lista todos os branches locais do git sem um asterisco

Andrew Miller
fonte
5

Eu itero, por exemplo:

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
fonte
4

Eu sugeriria $(git branch|grep -o "[0-9A-Za-z]\+")se suas filiais locais fossem nomeadas apenas por dígitos, az e / ou letras AZ

Xin Guo
fonte
4

A resposta aceita está correta e realmente deve ser a abordagem usada, mas resolver o problema no bash é um ótimo exercício para entender como os shells funcionam. O truque para fazer isso usando o bash sem realizar manipulação de texto adicional é garantir que a saída do git branch nunca seja expandida como parte de um comando a ser executado pelo shell. Isso evita que o asterisco se expanda na expansão do nome do arquivo (etapa 8) da expansão do shell (consulte http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_04.html )

Use a construção bash while com um comando read para dividir a saída do git branch em linhas. O '*' será lido como um caractere literal. Use uma instrução case para fazer a correspondência, prestando atenção especial aos padrões de correspondência.

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

Os asteriscos na construção do caso bash e na substituição do parâmetro precisam ser escapados com barras invertidas para evitar que o shell os interprete como caracteres de correspondência de padrão. Os espaços também são escapados (para evitar a tokenização) porque você está literalmente combinando '*'.

BitByteDog
fonte
4

A opção mais fácil de lembrar na minha opinião:

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

Resultado:

bamboo
develop
master

A opção -o do Grep (--only-matching) restringe a saída para apenas as partes correspondentes da entrada.

Visto que nem espaço nem * são válidos em nomes de ramificação Git, isso retorna a lista de ramificações sem os caracteres extras.

Editar: Se você estiver no estado de 'cabeça separada' , você precisará filtrar a entrada atual:

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

staafl
fonte
3

O que acabei fazendo, aplica-se à sua pergunta (& inspirado pela menção de ccpizza tr):

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

(Eu uso muito loops while. Embora para coisas específicas você definitivamente queira usar um nome de variável pontiagudo ["branch", por exemplo], na maioria das vezes estou apenas preocupado em fazer algo com cada linha de entrada. 'linha' aqui em vez de 'ramificação' é um aceno para a capacidade de reutilização / memória / eficiência muscular.)

Jan Kyu Peblik
fonte
1

Prosseguindo com a resposta de @finn (obrigado!), O seguinte permitirá que você itere nas ramificações sem criar um script de shell intermediário. É robusto o suficiente, desde que não haja novas linhas no nome do branch :)

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

O loop while é executado em um subshell, o que normalmente é bom, a menos que você esteja definindo variáveis ​​de shell que deseja acessar no shell atual. Nesse caso, você usa a substituição do processo para inverter o tubo:

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

Se você estiver neste estado:

git branch -a

* master

  remotes/origin/HEAD -> origin/master

  remotes/origin/branch1

  remotes/origin/branch2

  remotes/origin/branch3

  remotes/origin/master

E você executa 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        

Você obterá este resultado:

branch1

branch2

branch3

master
Wijith Bandara
fonte
Esta parece uma solução menos complexa para mim +1
Abhi
Isso funcionou para mim também:for b in "$(git branch)"; do git branch -D $b; done
Abhi
1

Mantenha simples

A maneira simples de obter o nome do branch em loop usando o script bash.

#!/bin/bash

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

Resultado:

master
other
Googliano
fonte
1

Resposta do Googlian, mas sem usar para

git for-each-ref --format='%(refname:lstrip=-1)' refs/heads/
Mestre Yoda
fonte
1
Isso não funciona para nomes de branch com espaço de nome, como em branches que possuem uma barra. Isso significa que os branches criados pelo dependabot, que se parecem com "dependabot / npm_and_yarn / typescript-3.9.5", aparecerão como "typescript-3.9.5".
ecbrodie
1
for branch in "$(git for-each-ref --format='%(refname:short)' refs/heads)"; do
    ...
done

Isso usa os comandos do git plumbing , que são projetados para scripts. Também é simples e padrão.

Referência: conclusão do Bash do Git

aude
fonte