Encontrando um ponto de ramificação com o Git?

454

Eu tenho um repositório com ramos mestre e A e muita atividade de mesclagem entre os dois. Como posso encontrar o commit no meu repositório quando o ramo A foi criado com base no mestre?

Meu repositório basicamente se parece com isso:

-- X -- A -- B -- C -- D -- F  (master) 
          \     /   \     /
           \   /     \   /
             G -- H -- I -- J  (branch A)

Estou procurando a revisão A, que não é o que git merge-base (--all)encontra.

Jochen
fonte

Respostas:

498

Eu estava procurando a mesma coisa e encontrei esta pergunta. Obrigado por perguntar!

No entanto, descobri que as respostas que eu vejo aqui não parecem bastante dar a resposta que você pediu (ou que eu estava procurando) - eles parecem dar a Gcomprometer, em vez do Acometer.

Então, eu criei a seguinte árvore (letras atribuídas em ordem cronológica), para que eu pudesse testar as coisas:

A - B - D - F - G   <- "master" branch (at G)
     \   \     /
      C - E --'     <- "topic" branch (still at E)

Isso parece um pouco diferente do seu, porque eu queria ter certeza (de referir-me a este gráfico, não o seu) B, mas não A (e não D ou E). Aqui estão as cartas anexadas aos prefixos SHA e às mensagens de confirmação (meu repositório pode ser clonado daqui , se isso for interessante para alguém):

G: a9546a2 merge from topic back to master
F: e7c863d commit on master after master was merged to topic
E: 648ca35 merging master onto topic
D: 37ad159 post-branch commit on master
C: 132ee2a first commit on topic branch
B: 6aafd7f second commit on master before branching
A: 4112403 initial commit on master

Assim, o objetivo: encontrar B . Aqui estão três maneiras que eu encontrei, depois de alguns ajustes:


1. visualmente, com gitk:

Você deve ver visualmente uma árvore como esta (como vista do mestre):

gitk screen capture from master

ou aqui (conforme exibido no tópico):

gitk screen capture from topic

nos dois casos, selecionei o commit que está Bno meu gráfico. Depois de clicar nele, seu SHA completo é apresentado em um campo de entrada de texto logo abaixo do gráfico.


2. visualmente, mas a partir do terminal:

git log --graph --oneline --all

(Editar / observação: adicionar --decoratetambém pode ser interessante; adiciona uma indicação de nomes de ramificações, tags etc.) Não adiciona isso à linha de comando acima, pois a saída abaixo não reflete seu uso.

que mostra (supondo git config --global color.ui auto):

output of git log --graph --oneline --all

Ou, em texto direto:

* a9546a2 mesclar do tópico de volta ao mestre
| \  
| * 648ca35 mesclando o mestre no tópico
| | \  
| * 132ee2a confirmado primeiro na ramificação de tópico
* | Confirmação de e7c863d no mestre depois que o mestre foi mesclado ao tópico
| | /  
| / |   
* 37ad159 confirmação pós-ramificação no mestre
| /  
* 6aafd7f segundo commit no master antes da ramificação
* 4112403 consolidação inicial no mestre

nos dois casos, vemos o commit 6aafd7f como o menor ponto comum, ou seja, Bno meu gráfico ou Ano seu.


3. Com casca mágica:

Você não especifica na sua pergunta se deseja algo como o descrito acima ou um único comando que fornecerá a única revisão e nada mais. Bem, aqui está o último:

diff -u <(git rev-list --first-parent topic) \
             <(git rev-list --first-parent master) | \
     sed -ne 's/^ //p' | head -1
6aafd7ff98017c816033df18395c5c1e7829960d

Que você também pode colocar no seu ~ / .gitconfig como (nota: o traço final é importante; obrigado Brian por chamar a atenção para isso) :

[alias]
    oldest-ancestor = !zsh -c 'diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne \"s/^ //p\" | head -1' -

O que poderia ser feito através da seguinte linha de comando (complicada entre aspas):

git config --global alias.oldest-ancestor '!zsh -c '\''diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne "s/^ //p" | head -1'\'' -'

Nota: zshpoderia facilmente ter sido bash, mas shserá não trabalho - a <()sintaxe não existe no baunilha sh. (Obrigado novamente, @conny, por me informar sobre isso em um comentário em outra resposta nesta página!)

Nota: Versão alternativa do acima:

Obrigado a liori por apontar que o acima pode cair ao comparar ramos idênticos e criar uma forma diff diferente que remove a forma sed da mistura e torna isso "mais seguro" (isto é, retorna um resultado (ou seja, o confirmação mais recente), mesmo quando você compara mestre a mestre):

Como uma linha .git-config:

[alias]
    oldest-ancestor = !zsh -c 'diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1' -

Do shell:

git config --global alias.oldest-ancestor '!zsh -c '\''diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1'\'' -'

Então, na minha árvore de teste (que ficou indisponível por um tempo, desculpe; está de volta), que agora funciona tanto no mestre quanto no tópico (dando confirmações G e B, respectivamente). Mais uma vez obrigado, liori, pela forma alternativa.


Então, foi isso que eu [e liori] propus. Parece funcionar para mim. Também permite alguns aliases adicionais que podem ser úteis:

git config --global alias.branchdiff '!sh -c "git diff `git oldest-ancestor`.."'
git config --global alias.branchlog '!sh -c "git log `git oldest-ancestor`.."'

Git-ing feliz!

lindes
fonte
5
Obrigado lindes, a opção shell é ótima para situações em que você deseja encontrar o ponto de ramificação de uma ramificação de manutenção de longa execução. Quando você está procurando uma revisão que possa ter mil confirmações no passado, as opções visuais realmente não serão suficientes. * 8 ')
Mark Booth
4
No seu terceiro método, você depende de que o contexto mostrará a primeira linha inalterada. Isso não acontecerá em certos casos extremos ou se você tiver requisitos ligeiramente diferentes (por exemplo, eu preciso que apenas um dos históricos seja --primeiro pai, e eu estou usando esse método em um script que às vezes pode usar o mesmo ramos nos dois lados). Achei mais seguro usar diffé if-then-else modo e apagar alteradas / deletadas linhas de sua saída em vez de contar em ter contexto grande o suficiente, por.: diff --old-line-format='' --new-line-format='' <(git rev-list …) <(git rev-list …)|head -1.
Liori 5/10
3
git log -n1 --format=format:%H $(git log --reverse --format=format:%H master..topic | head -1)~funcionará também, eu acho #
Tobias Schulte
4
@ JakubNarębski: Você tem como git merge-base --fork-point ...dar o commit B (commit 6aafd7f) para esta árvore? Eu estava ocupado com outras coisas quando você postou isso, e parecia bom, mas finalmente tentei e não estou conseguindo fazer o trabalho ... Ou recebo algo mais recente ou apenas uma falha silenciosa (sem erro mensagem, mas status de saída 1), tentando argumentos como git co master; git merge-base --fork-point topic, git co topic; git merge-base --fork-point master, git merge-base --fork-point topic master(quer para checkout), etc. existe algo que eu estou fazendo de errado ou faltando?
Lindes
25
@ JakubNarębski @lindes --fork-pointé baseado no reflog, portanto, só funcionará se você tiver feito as alterações localmente. Mesmo assim, as entradas do reflog poderiam ter expirado. É útil, mas não confiável.
Daniel Lubarov
131

Você pode estar procurando git merge-base:

O git merge-base encontra os melhores ancestrais comuns entre dois commits para usar em uma fusão de três vias. Um ancestral comum é melhor que outro ancestral comum se o último for um ancestral do primeiro. Um ancestral comum que não possui um ancestral comum melhor é o melhor ancestral comum , ou seja, uma base de mesclagem . Observe que pode haver mais de uma base de mesclagem para um par de confirmações.

Greg Hewgill
fonte
10
Observe também a --allopção " git merge-base"
Jakub Narębski
10
Isso não responde à pergunta original, mas a maioria das pessoas faz a pergunta mais simples para a qual essa é a resposta :)
FelipeC
6
ele disse que não queria o resultado da base de mesclagem do git
Tom Tanner
9
@ TomTanner: Acabei de examinar o histórico da pergunta e a pergunta original foi editada para incluir essa nota cerca de git merge-basecinco horas após a publicação da minha resposta (provavelmente em resposta à minha resposta). No entanto, deixarei esta resposta como está, pois ainda pode ser útil para outra pessoa que encontrar essa pergunta por meio de pesquisa.
Greg Hewgill
Acredito que você possa usar os resultados da mesclagem-base - todos e depois examinar a lista de confirmações de retorno usando a mesclagem-base - é-ancestral para encontrar o ancestral comum mais antigo da lista, mas essa não é a maneira mais simples .
91118 Matt_G
42

Eu usei git rev-listpara esse tipo de coisa. Por exemplo, (observe os 3 pontos)

$ git rev-list --boundary branch-a...master | grep "^-" | cut -c2-

cuspirá o ponto de ramificação. Agora, não é perfeito; desde que você mesclou o mestre na ramificação A algumas vezes, isso dividirá alguns pontos de ramificação possíveis (basicamente, o ponto de ramificação original e, em seguida, cada ponto no qual você mesclou o mestre na ramificação A). No entanto, deve pelo menos diminuir as possibilidades.

Eu adicionei esse comando aos meus aliases em ~/.gitconfig :

[alias]
    diverges = !sh -c 'git rev-list --boundary $1...$2 | grep "^-" | cut -c2-'

para que eu possa chamá-lo como:

$ git diverges branch-a master
mipadi
fonte
Nota: isso parece fornecer o primeiro commit no ramo, em vez do ancestral comum. (ou seja, ele fornece, em Gvez de A, de acordo com o gráfico da pergunta original.) Tenho uma resposta que recebo A, que postarei no momento.
Lindes
@ Lindes: Dá o ancestral comum em todos os casos que eu tentei. Você tem um exemplo em que isso não acontece?
Mipadi
Sim. Na minha resposta (que tem um link para um repositório que você pode clonar; git checkout topice executá-lo topicno lugar de branch-a), ele lista 648ca357b946939654da12aaf2dc072763f3caeee 37ad15952db5c065679d7fc31838369712f0b338- ambos 37ad159e 648ca35estão no ancestral dos ramos atuais (o último sendo o atual HEAD de topic), mas também não é o ponto antes da ramificação. Você tem algo diferente?
Lindes
@ Lindes: Não foi possível clonar seu repositório (possivelmente um problema de permissão?).
Mipadi
OPA, desculpe! Obrigado por me avisar. Eu esqueci de executar o git update-server-info. Deve ser bom ir agora. :)
Lindes
31

Se você gosta de comandos concisos,

git rev-list $ (git rev-list - primeiro pai ^ mestre do branch_name | tail -n1) ^^! 

Aqui está uma explicação.

O comando a seguir fornece a lista de todas as confirmações no mestre que ocorreram após a criação do branch_name

git rev-list --primeiro-pai ^ branch_name master 

Como você se preocupa apenas com as confirmações mais antigas, deseja a última linha da saída:

git rev-list ^ branch_name - mestre primeiro pai | cauda -n1

O pai do commit mais antigo que não é um ancestral de "branch_name" está, por definição, em "branch_name" e está em "master", pois é um ancestral de algo em "master". Então você tem o commit mais antigo que existe nos dois ramos.

O comando

git rev-list commit ^^!

é apenas uma maneira de mostrar a referência de confirmação pai. Você poderia usar

git log -1 commit ^

como queiras.

PS: Não concordo com o argumento de que a ordem dos ancestrais é irrelevante. Depende do que você quer. Por exemplo, neste caso

_C1___C2_______ master
  \ \ _XXXXX_ ramificação A (os Xs indicam cruzamentos arbitrários entre mestre e A)
   \ _____ / filial B

faz todo o sentido emitir C2 como o commit "ramificado". É quando o desenvolvedor se ramifica de "mestre". Quando ele se ramificou, o ramo "B" nem sequer foi fundido em seu ramo! É isso que a solução deste post fornece.

Se o que você deseja é o último commit C, de modo que todos os caminhos da origem até o último commit no ramo "A" passem por C, então você deseja ignorar a ordem de ancestralidade. Isso é puramente topológico e dá uma idéia de quando você tem duas versões do código funcionando ao mesmo tempo. É quando você usa abordagens baseadas em mesclagem, e ele retornará C1 no meu exemplo.

Lionel
fonte
3
Esta é, de longe, a resposta mais limpa, vamos votá-la no topo. A edição sugeriu: git rev-list commit^^!pode ser simplificada comogit rev-parse commit^
Russell Davis
Essa deve ser a resposta!
Trinth
2
Essa resposta é bom, eu apenas substituído git rev-list --first-parent ^branch_name mastercom git rev-list --first-parent branch_name ^masterporque se o branch master é 0 commits à frente do outro ramo (fast-forwardable a ele), nenhuma saída seria criado. Com minha solução, nenhuma saída é criada se o mestre estiver estritamente à frente (ou seja, o ramo foi totalmente mesclado), que é o que eu quero.
Michael Schmeißer
3
Isso não funcionará, a menos que eu esteja totalmente faltando alguma coisa. Existem mesclagens em ambas as direções nos ramos de exemplo. Parece que você tentou levar isso em consideração, mas acredito que isso fará com que sua resposta falhe. git rev-list --first-parent ^topic mastersó o levará de volta ao primeiro commit após a última mesclagem de masterpara topic(se isso existir).
jerry
1
@ jerry Você está correto, esta resposta é lixo; por exemplo, no caso de uma retaguarda ter acabado de ocorrer (mestre mesclado no tópico) e o mestre não ter novas confirmações depois disso, o primeiro comando git rev-list --first-parent não produz nada.
TamaMcGlinn 17/04/19
19

Dado que muitas das respostas neste segmento não dão a resposta que a pergunta estava solicitando, aqui está um resumo dos resultados de cada solução, juntamente com o script que eu usei para replicar o repositório fornecido na pergunta.

O registro

Criando um repositório com a estrutura fornecida, obtemos o log do git de:

$ git --no-pager log --graph --oneline --all --decorate
* b80b645 (HEAD, branch_A) J - Work in branch_A branch
| *   3bd4054 (master) F - Merge branch_A into branch master
| |\  
| |/  
|/|   
* |   a06711b I - Merge master into branch_A
|\ \  
* | | bcad6a3 H - Work in branch_A
| | * b46632a D - Work in branch master
| |/  
| *   413851d C - Merge branch_A into branch master
| |\  
| |/  
|/|   
* | 6e343aa G - Work in branch_A
| * 89655bb B - Work in branch master
|/  
* 74c6405 (tag: branch_A_tag) A - Work in branch master
* 7a1c939 X - Work in branch master

Minha única adição é a tag que a torna explícita sobre o ponto em que criamos o branch e, portanto, o commit que desejamos encontrar.

A solução que funciona

A única solução que funciona é a fornecida por lindes retorna corretamente A:

$ diff -u <(git rev-list --first-parent branch_A) \
          <(git rev-list --first-parent master) | \
      sed -ne 's/^ //p' | head -1
74c6405d17e319bd0c07c690ed876d65d89618d5

Como Charles Bailey aponta, porém, essa solução é muito frágil.

Se você branch_Aentrar mastere depois masterentrar branch_Asem comprometer a intervenção, a solução de Lindes apenas fornecerá a primeira divergência mais recente .

Isso significa que, para o meu fluxo de trabalho, acho que vou ter que marcar o ponto de ramificação de ramificações de execução longa, pois não posso garantir que elas possam ser encontradas mais tarde.

Isso realmente tudo se resume a gits falta do que hgchama ramos nomeados . O JHW blogueiro chama essas linhagens vs. famílias em seu artigo porque eu gosto Mercurial More Than Git e seu artigo de follow-up Mais sobre Mercurial vs. Git (com gráficos!) . Eu recomendaria pessoas lê-los para ver por que alguns convertidos mercuriais não perca ter chamado ramos em git.

As soluções que não funcionam

A solução fornecida pelo mipadi retorna duas respostas Ie C:

$ git rev-list --boundary branch_A...master | grep ^- | cut -c2-
a06711b55cf7275e8c3c843748daaa0aa75aef54
413851dfecab2718a3692a4bba13b50b81e36afc

A solução fornecida por Greg Hewgill retornaI

$ git merge-base master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54
$ git merge-base --all master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54

A solução fornecida por Karl retorna X:

$ diff -u <(git log --pretty=oneline branch_A) \
          <(git log --pretty=oneline master) | \
       tail -1 | cut -c 2-42
7a1c939ec325515acfccb79040b2e4e1c3e7bbe5

O script

mkdir $1
cd $1
git init
git commit --allow-empty -m "X - Work in branch master"
git commit --allow-empty -m "A - Work in branch master"
git branch branch_A
git tag branch_A_tag     -m "Tag branch point of branch_A"
git commit --allow-empty -m "B - Work in branch master"
git checkout branch_A
git commit --allow-empty -m "G - Work in branch_A"
git checkout master
git merge branch_A       -m "C - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "H - Work in branch_A"
git merge master         -m "I - Merge master into branch_A"
git checkout master
git commit --allow-empty -m "D - Work in branch master"
git merge branch_A       -m "F - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "J - Work in branch_A branch"

Duvido que a versão git faça muita diferença para isso, mas:

$ git --version
git version 1.7.1

Agradeço a Charles Bailey por me mostrar uma maneira mais compacta de criar scripts para o repositório de exemplo.

Mark Booth
fonte
2
A solução de Karl é fácil de resolver: diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1. Obrigado por fornecer instruções para criar o repo :)
FelipeC
Eu acho que você quer dizer "A variação limpa da solução fornecida por Karl retorna X". O funcionou bem originais era apenas feio :-)
Karl
Não, o seu original não funciona bem. Concedido, a variação funciona ainda pior. Mas adicionando a opção --topo-fim faz o seu trabalho versão :)
FelipeC
@felipec - Veja meu comentário final sobre a resposta de Charles Bailey . Infelizmente, nosso bate-papo (e, portanto, todos os comentários antigos) foram excluídos. Vou tentar atualizar minha resposta quando chegar a hora.
Mark Booth
Interessante. Eu meio que assumi que topológico era o padrão. Tolo me :-)
Karl
10

Em geral, isso não é possível. No histórico de uma ramificação, uma ramificação e mesclagem antes que uma ramificação nomeada fosse ramificada e uma ramificação intermediária de duas ramificações nomeadas são iguais.

No git, ramificações são apenas os nomes atuais das dicas de seções da história. Eles realmente não têm uma identidade forte.

Isso geralmente não é um grande problema, pois a base de mesclagem (consulte a resposta de Greg Hewgill) de dois commits geralmente é muito mais útil, fornecendo o commit mais recente compartilhado pelos dois ramos.

Uma solução que depende da ordem dos pais de uma consolidação obviamente não funcionará em situações em que uma ramificação foi totalmente integrada em algum momento do histórico da ramificação.

git commit --allow-empty -m root # actual branch commit
git checkout -b branch_A
git commit --allow-empty -m  "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git merge --ff-only master
git commit --allow-empty -m "More work on branch_A"
git checkout master
git commit --allow-empty -m "More work on master"

Essa técnica também cai se uma mesclagem de integração tiver sido feita com os pais revertidos (por exemplo, uma ramificação temporária foi usada para executar uma mesclagem de teste no mestre e, em seguida, avançou rapidamente na ramificação do recurso para aumentar ainda mais).

git commit --allow-empty -m root # actual branch point
git checkout -b branch_A
git commit --allow-empty -m  "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git commit --allow-empty -m "More work on branch_A"

git checkout -b tmp-branch master
git merge -m "Merge branch_A into tmp-branch (master copy)" branch_A
git checkout branch_A
git merge --ff-only tmp-branch
git branch -d tmp-branch

git checkout master
git commit --allow-empty -m "More work on master"
CB Bailey
fonte
3
Obrigado Charles, você me convenceu, se eu quiser saber o ponto em que o ramo originalmente divergiu , vou ter que marcá-lo. Eu realmente desejo que gittinha um equivalente ao hg's chamado ramos, seria tornar o gerenciamento de ramos de manutenção de longa vida de modo muito mais fácil.
perfil completo de Marcos Booth
1
"No git, os ramos são apenas os nomes atuais das dicas das seções da história. Eles realmente não têm uma identidade forte" Isso é algo assustador de se dizer e me convenceu de que preciso entender melhor os ramos do Git - obrigado (+ 1)
dumbledad 13/10/2015
No histórico de uma ramificação, uma ramificação e mesclagem antes que uma ramificação nomeada fosse ramificada e uma ramificação intermediária de duas ramificações nomeadas são iguais. Sim. +1.
User1071847
5

Que tal algo como

git log --pretty=oneline master > 1
git log --pretty=oneline branch_A > 2

git rev-parse `diff 1 2 | tail -1 | cut -c 3-42`^
Karl
fonte
Isso funciona. É realmente complicado, mas é a única coisa que eu descobri que realmente parece fazer o trabalho.
GaryO
1
Equivalente no alias do Git: diverges = !bash -c 'git rev-parse $(diff <(git log --pretty=oneline ${1}) <(git log --pretty=oneline ${2}) | tail -1 | cut -c 3-42)^'(sem arquivos temporários)
conny
@conny: Oh, uau - eu nunca tinha visto a sintaxe <(foo) ... isso é incrivelmente útil, obrigado! (Funciona no zsh também, para sua informação.) #
Lindes
isso parece me dar o primeiro commit no ramo, em vez do ancestral comum. (ou seja, ele fornece, em Gvez de A, de acordo com o gráfico da pergunta original.) Acho que encontrei uma resposta, que postarei atualmente.
Lindes
Em vez de 'git log --pretty = oneline', você pode simplesmente fazer 'git rev-list', e também pode pular o corte; além disso, isso dá ao pai o comprometimento do ponto de divergência, então apenas siga -2 | head 1. Então:diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1
FelipeC
5

Certamente estou perdendo alguma coisa, mas na IMO, todos os problemas acima são causados ​​porque estamos sempre tentando encontrar o ponto de ramificação que remonta à história e isso causa todos os tipos de problemas devido às combinações disponíveis.

Em vez disso, segui uma abordagem diferente, com base no fato de que ambas as filiais compartilham muito histórico, exatamente todo o histórico antes da ramificação é 100% o mesmo. Portanto, em vez de voltar, minha proposta é seguir em frente (a partir de 1º commit), procurando a 1ª diferença nos dois ramos. O ponto de ramificação será, simplesmente, o pai da primeira diferença encontrada.

Na prática:

#!/bin/bash
diff <( git rev-list "${1:-master}" --reverse --topo-order ) \
     <( git rev-list "${2:-HEAD}" --reverse --topo-order) \
--unified=1 | sed -ne 's/^ //p' | head -1

E está resolvendo todos os meus casos habituais. Claro que existem fronteiras que não são cobertas, mas ... ciao :-)

stronk7
fonte
diff <(lista rev. git "$ {1: -master}" --primeiro pai) <(lista rev. git "$ {2: -HEAD}" --primeiro pai) -U1 | tail -1
Alexander Bird
i encontrado que este seja mais rápido (2-100x):comm --nocheck-order -1 -2 <(git rev-list --reverse --topo-order topic) <(git rev-list --reverse --topo-order master) | head -1
Andrei Neculau
4

Depois de muitas pesquisas e discussões, fica claro que não há nenhuma bala mágica que funcionaria em todas as situações, pelo menos não na versão atual do Git.

Foi por isso que escrevi alguns patches que adicionam o conceito de tailramo. Cada vez que uma ramificação é criada, também é criado um ponteiro para o ponto original, a tailref. Essa referência é atualizada toda vez que a ramificação é rebaseada.

Para descobrir o ponto de ramificação do ramo de desenvolvimento, tudo que você precisa fazer é usar devel@{tail}, é isso.

https://github.com/felipec/git/commits/fc/tail

FelipeC
fonte
1
Pode ser a única solução estável. Você viu se isso poderia entrar no git? Não vi uma solicitação de recebimento.
Alexander Klimetschek
@AlexanderKlimetschek Não enviei os patches e acho que eles não seriam aceitos. No entanto, tentei um método diferente: um gancho "update-branch" que faz algo muito semelhante. Dessa forma, por padrão, o Git não faria nada, mas você poderia ativar o gancho para atualizar o ramo da cauda. Você não teria desenvolvido @ {tail}, mas não seria tão ruim usar tails / devel.
FelipeC
3

Aqui está uma versão melhorada da minha resposta anterior resposta anterior . Ele se baseia nas mensagens de confirmação das mesclagens para descobrir onde a ramificação foi criada pela primeira vez.

Ele funciona em todos os repositórios mencionados aqui, e eu até resolvi alguns que surgiram na lista de discussão . Eu também escrevi testes para isso.

find_merge ()
{
    local selection extra
    test "$2" && extra=" into $2"
    git rev-list --min-parents=2 --grep="Merge branch '$1'$extra" --topo-order ${3:---all} | tail -1
}

branch_point ()
{
    local first_merge second_merge merge
    first_merge=$(find_merge $1 "" "$1 $2")
    second_merge=$(find_merge $2 $1 $first_merge)
    merge=${second_merge:-$first_merge}

    if [ "$merge" ]; then
        git merge-base $merge^1 $merge^2
    else
        git merge-base $1 $2
    fi
}
FelipeC
fonte
3

O comando a seguir revelará o SHA1 do Commit A

git merge-base --fork-point A

Gayan Pathirage
fonte
Isso não acontece se os ramos pai e filho tiverem intercalações intermediárias entre si.
Nawfal # 20/18
2

Parece que estou tendo alguma alegria com

git rev-list branch...master

A última linha que você obtém é a primeira confirmação no ramo, então é uma questão de obter o pai dela. assim

git rev-list -1 `git rev-list branch...master | tail -1`^

Parece funcionar para mim e não precisa de diffs e assim por diante (o que é útil, pois não temos essa versão do diff)

Correção: isso não funciona se você estiver no ramo principal, mas estou fazendo isso em um script, o que é menos problemático

Tom Tanner
fonte
2

Para encontrar confirmações a partir do ponto de ramificação, você pode usar isso.

git log --ancestry-path master..topicbranch
Andor
fonte
Este comando não funciona para mim no exemplo dado. Por favor, o que você forneceria como parâmetros para o intervalo de confirmação?
Jesper Rønn-Jensen
2

Às vezes, é efetivamente impossível (com algumas exceções de onde você pode ter sorte de ter dados adicionais) e as soluções aqui não funcionam.

O Git não preserva o histórico de referências (que inclui ramificações). Ele armazena apenas a posição atual de cada filial (a cabeça). Isso significa que você pode perder algum histórico de ramificação no git ao longo do tempo. Sempre que você ramifica, por exemplo, perde-se imediatamente qual ramificação era a original. Tudo o que um ramo faz é:

git checkout branch1    # refs/branch1 -> commit1
git checkout -b branch2 # branch2 -> commit1

Você pode supor que o primeiro comprometido seja o ramo. Isso costuma ser o caso, mas nem sempre é assim. Não há nada que impeça você de se comprometer com qualquer ramificação primeiro após a operação acima. Além disso, não é garantido que os carimbos de data e hora do git sejam confiáveis. Não é até você se comprometer com os dois que eles realmente se tornam ramificações estruturalmente.

Enquanto nos diagramas tendemos a numerar confirmações conceitualmente, o git não tem um conceito realmente estável de sequência quando a árvore de confirmação se ramifica. Nesse caso, você pode assumir que os números (indicando a ordem) são determinados pelo registro de data e hora (pode ser divertido ver como uma interface do usuário do git lida com as coisas quando você define todos os registros de data e hora para o mesmo).

Isto é o que um humano espera conceitualmente:

After branch:
       C1 (B1)
      /
    -
      \
       C1 (B2)
After first commit:
       C1 (B1)
      /
    - 
      \
       C1 - C2 (B2)

Isto é o que você realmente obtém:

After branch:
    - C1 (B1) (B2)
After first commit (human):
    - C1 (B1)
        \
         C2 (B2)
After first commit (real):
    - C1 (B1) - C2 (B2)

Você assumiria que B1 é o ramo original, mas poderia ser simplesmente um ramo morto (alguém fez o checkout -b, mas nunca se comprometeu com ele). Não é até você se comprometer com os dois que você obtém uma estrutura de ramificação legítima no git:

Either:
      / - C2 (B1)
    -- C1
      \ - C3 (B2)
Or:
      / - C3 (B1)
    -- C1
      \ - C2 (B2)

Você sempre sabe que C1 veio antes de C2 e C3, mas nunca sabe com segurança se C2 veio antes de C3 ou C3 antes de C2 (porque você pode definir o horário da estação de trabalho para qualquer coisa, por exemplo). B1 e B2 também são enganosos, pois você não pode saber qual ramificação veio primeiro. Você pode adivinhar muito bem e geralmente preciso em muitos casos. É um pouco como uma pista de corrida. Todas as coisas geralmente são iguais aos carros, então você pode assumir que um carro que vem atrás de uma volta começou uma volta atrás. Também temos convenções muito confiáveis, por exemplo, o mestre quase sempre representa os ramos mais antigos, embora, infelizmente, eu tenha visto casos em que mesmo esse não é o caso.

O exemplo fornecido aqui é um exemplo de preservação de histórico:

Human:
    - X - A - B - C - D - F (B1)
           \     / \     /
            G - H ----- I - J (B2)
Real:
            B ----- C - D - F (B1)
           /       / \     /
    - X - A       /   \   /
           \     /     \ /
            G - H ----- I - J (B2)

O real aqui também é enganoso, porque nós, como humanos, o lemos da esquerda para a direita, da raiz à folha (ref). Git não faz isso. Onde fazemos (A-> B) em nossas cabeças, o git faz (A <-B ou B-> A). Ele lê de ref para root. As referências podem estar em qualquer lugar, mas tendem a ser folhas, pelo menos para ramos ativos. Um ref aponta para um commit e commit apenas contém um like para seus pais, não para seus filhos. Quando um commit é um commit de mesclagem, ele terá mais de um pai. O primeiro pai é sempre o commit original que foi mesclado. Os outros pais sempre são confirmados que foram mesclados no commit original.

Paths:
    F->(D->(C->(B->(A->X)),(H->(G->(A->X))))),(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))
    J->(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))

Esta não é uma representação muito eficiente, mas uma expressão de todos os caminhos que o git pode seguir de cada referência (B1 e B2).

O armazenamento interno do Git se parece mais com isso (não que A como pai apareça duas vezes):

    F->D,I | D->C | C->B,H | B->A | A->X | J->I | I->H,C | H->G | G->A

Se você despejar um commit git bruto, verá zero ou mais campos pai. Se houver zero, significa que não há pai e o commit é uma raiz (você pode realmente ter várias raízes). Se houver um, significa que não houve mesclagem e não é um commit raiz. Se houver mais de um, significa que o commit é o resultado de uma mesclagem e todos os pais após o primeiro são mesclados.

Paths simplified:
    F->(D->C),I | J->I | I->H,C | C->(B->A),H | H->(G->A) | A->X
Paths first parents only:
    F->(D->(C->(B->(A->X)))) | F->D->C->B->A->X
    J->(I->(H->(G->(A->X))) | J->I->H->G->A->X
Or:
    F->D->C | J->I | I->H | C->B->A | H->G->A | A->X
Paths first parents only simplified:
    F->D->C->B->A | J->I->->G->A | A->X
Topological:
    - X - A - B - C - D - F (B1)
           \
            G - H - I - J (B2)

Quando os dois atingem A, a cadeia será a mesma, antes disso a cadeia será totalmente diferente. O primeiro commit que outros dois commits têm em comum é o ancestral comum e de onde eles divergem. pode haver alguma confusão aqui entre os termos commit, branch e ref. Na verdade, você pode mesclar um commit. É isso que a mesclagem realmente faz. Uma ref simplesmente aponta para um commit e uma ramificação nada mais é que uma ref na pasta .git / refs / heads, o local da pasta é o que determina que uma ref é uma ramificação e não algo como uma tag.

Onde você perde a história é que a mesclagem fará uma de duas coisas, dependendo das circunstâncias.

Considerar:

      / - B (B1)
    - A
      \ - C (B2)

Nesse caso, uma mesclagem em qualquer direção criará um novo commit com o primeiro pai como o commit apontado pelo ramo atual com check-out e o segundo pai como o commit na ponta do ramo que você mesclou em seu ramo atual. Ele precisa criar um novo commit, pois os dois ramos têm alterações desde o ancestral comum que deve ser combinado.

      / - B - D (B1)
    - A      /
      \ --- C (B2)

Neste ponto, D (B1) agora possui os dois conjuntos de alterações de ambos os ramos (ele mesmo e B2). No entanto, o segundo ramo não possui as alterações de B1. Se você mesclar as alterações de B1 em B2 para que elas sejam sincronizadas, você pode esperar algo parecido com isso (você pode forçar o git merge a fazer isso dessa maneira, porém com --no-ff):

Expected:
      / - B - D (B1)
    - A      / \
      \ --- C - E (B2)
Reality:
      / - B - D (B1) (B2)
    - A      /
      \ --- C

Você conseguirá isso mesmo que B1 tenha confirmações adicionais. Enquanto não houver alterações em B2 que B1 não possua, as duas ramificações serão mescladas. Ele faz um avanço rápido que é como uma rebase (as rebotes também comem ou linearizam o histórico), exceto ao contrário de uma rebase, pois apenas uma ramificação tem um conjunto de alterações, não é necessário aplicar um conjunto de alterações de uma ramificação em cima da outra.

From:
      / - B - D - E (B1)
    - A      /
      \ --- C (B2)
To:
      / - B - D - E (B1) (B2)
    - A      /
      \ --- C

Se você parar de trabalhar no B1, tudo ficará bem em preservar a história a longo prazo. Somente B1 (que pode ser mestre) avançará normalmente, portanto o local de B2 no histórico de B2 representa com êxito o ponto em que foi mesclado em B1. Isto é o que o git espera que você faça, para ramificar B de A e, em seguida, você pode mesclar A em B o quanto quiser enquanto as mudanças se acumulam, no entanto, ao mesclar B de volta a A, não é esperado que você trabalhe em B e mais . Se você continuar trabalhando em sua ramificação após avançar rapidamente, incorporando-a novamente à ramificação em que estava trabalhando, apagará o histórico anterior de B todas as vezes. Você está realmente criando uma nova ramificação toda vez que o avanço rápido é confirmado na origem e depois confirmado na ramificação.

         0   1   2   3   4 (B1)
        /-\ /-\ /-\ /-\ /
    ----   -   -   -   -
        \-/ \-/ \-/ \-/ \
         5   6   7   8   9 (B2)

1 a 3 e 5 a 8 são ramificações estruturais que aparecem se você seguir o histórico de 4 ou 9. Não há como o git saber a qual desses ramificações estruturais sem nome e sem referência pertencem os ramos nomeados e de referência como o fim da estrutura. Você pode supor neste desenho que 0 a 4 pertence a B1 e 4 a 9 pertence a B2, mas, além de 4 e 9, não era possível saber qual ramo pertence a qual ramo, eu simplesmente o desenhei de uma maneira que ilusão disso. 0 pode pertencer a B2 e 5 pode pertencer a B1. Existem 16 possibilidades diferentes nesse caso, das quais ramificações nomeadas às quais cada uma das ramificações estruturais poderia pertencer.

Existem várias estratégias git que resolvem isso. Você pode forçar o git merge a nunca avançar rapidamente e sempre criar um branch de mesclagem. Uma maneira horrível de preservar o histórico de ramificações é com tags e / ou ramificações (as tags são realmente recomendadas), de acordo com algumas convenções de sua escolha. Eu realmente não recomendaria um commit vazio no ramo em que você está se fundindo. Uma convenção muito comum é não mesclar em uma ramificação de integração até que você queira realmente fechar sua ramificação. Essa é uma prática que as pessoas devem tentar aderir, caso contrário você está trabalhando em torno de ter ramificações. No entanto, no mundo real, o ideal nem sempre é um significado prático; fazer a coisa certa não é viável para todas as situações. Se o que você '

jgmjgm
fonte
"O Git não preserva o histórico de referências" Sim, mas não por padrão e não por muito tempo. Veja man git-refloge a parte sobre datas: "master@{one.week.ago} significa" onde o mestre costumava apontar uma semana atrás neste repositório local "". Ou a discussão <refname>@{<date>}em man gitrevisions. E core.reflogExpiredentro man git-config.
Patrick Mevzek 12/03/19
2

Não é uma solução para a pergunta, mas achei que valeria a pena observar a abordagem que utilizo quando tenho um ramo de vida longa:

Ao mesmo tempo em que crio a ramificação, também crio uma tag com o mesmo nome, mas com um -initsufixo, por exemplo, feature-branche feature-branch-init.

(É meio bizarro que essa seja uma pergunta tão difícil de responder!)

Stephen Darlington
fonte
1
Considerando a pura estupidez estonteante de projetar um conceito de "ramo" sem a noção de quando e onde ele foi criado ... mais as imensas complicações de outras soluções sugeridas - por pessoas que tentam ser espertas demais demais acho que prefiro sua solução. Somente isso sobrecarrega a necessidade de lembrar-se de fazer isso toda vez que você cria um ramo - algo que os usuários do git estão fazendo com muita frequência. Além disso - eu li em algum lugar que 'tag tem uma penalidade de ser' pesado '. Ainda assim, acho que é o que acho que farei.
Motti Shneor
1

Uma maneira simples de facilitar a visualização do ponto de ramificação git log --graphé usar a opção--first-parent .

Por exemplo, aceite o repositório com a resposta aceita :

$ git log --all --oneline --decorate --graph

*   a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
|\  
| *   648ca35 (origin/topic) merging master onto topic
| |\  
| * | 132ee2a first commit on topic branch
* | | e7c863d commit on master after master was merged to topic
| |/  
|/|   
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

Agora adicione --first-parent:

$ git log --all --oneline --decorate --graph --first-parent

* a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
| * 648ca35 (origin/topic) merging master onto topic
| * 132ee2a first commit on topic branch
* | e7c863d commit on master after master was merged to topic
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

Isso facilita!

Observe que se o repositório tiver muitos ramos, você desejará especificar os 2 ramos que você está comparando em vez de usar --all:

$ git log --decorate --oneline --graph --first-parent master origin/topic
binaryfunt
fonte
0

O problema parece ser o de encontrar o corte mais recente e de confirmação única entre os dois ramos de um lado e o ancestral comum mais antigo do outro (provavelmente o commit inicial do repo). Isso corresponde à minha intuição de qual é o ponto "ramificação".

Assim, isso não é nada fácil de calcular com os comandos normais do git shell, já que git rev-list- nossa ferramenta mais poderosa - não nos permite restringir o caminho pelo qual um commit é alcançado. O mais próximo que temos é git rev-list --boundary, o que pode nos dar um conjunto de todos os commits que "bloquearam nosso caminho". (Nota:git rev-list --ancestry-path é interessante, mas não sei como torná-lo útil aqui.)

Aqui está o script: https://gist.github.com/abortz/d464c88923c520b79e3d . É relativamente simples, mas devido a um loop, é complicado o suficiente para justificar uma essência.

Observe que a maioria das outras soluções propostas aqui não pode funcionar em todas as situações por um simples motivo: git rev-list --first-parent não é confiável no histórico de linearização, pois pode haver mesclagens em qualquer um dos pedidos.

git rev-list --topo-order, por outro lado, é muito útil - para realizar commits em ordem topográfica -, mas fazer diffs é frágil: existem várias ordens topográficas possíveis para um determinado gráfico, portanto, você depende de uma certa estabilidade das ordens. Dito isto, a solução da strongk7 provavelmente funciona muito bem na maioria das vezes. No entanto, é mais lento que o meu como resultado de ter que percorrer toda a história do repositório ... duas vezes. :-)

Andrew Bortz
fonte
0

O seguinte implementa o equivalente git do svn log --stop-on-copy e também pode ser usado para encontrar a origem da ramificação.

Aproximação

  1. Obter cabeça para todos os ramos
  2. coletar mergeBase para ramificação de destino
  3. git.log e iterar
  4. Pare na primeira confirmação que aparece na lista mergeBase

Como todos os rios correm para o mar, todos os galhos correm para dominar e, portanto, encontramos uma base de fusão entre galhos aparentemente não relacionados. À medida que voltamos da cabeça do ramo através dos ancestrais, podemos parar na primeira base de mesclagem potencial, pois, em teoria, deveria ser o ponto de origem desse ramo.

Notas

  • Não tentei essa abordagem em que os ramos de irmãos e primos se fundiram.
  • Eu sei que deve haver uma solução melhor.

detalhes: https://stackoverflow.com/a/35353202/9950

Peter Kahn
fonte
-1

Você pode usar o seguinte comando para retornar a confirmação mais antiga em branch_a, que não pode ser acessada pelo mestre:

git rev-list branch_a ^master | tail -1

Talvez com uma verificação de sanidade adicional, verifique se o pai desse commit é realmente acessível pelo mestre ...

Marc Richarme
fonte
1
Isso não funciona. Se branch_a for mesclado no master uma vez e continuar, os commits nessa mesclagem serão considerados parte do master, para que não apareçam no ^ master.
FelipeC
-2

Você pode examinar o reflog do ramo A para descobrir a partir de qual commit ele foi criado, bem como o histórico completo do qual o commit apontou. Os registros estão em .git/logs.

Paggas
fonte
2
Eu não acho que isso funcione em geral porque o reflog pode ser podado. E também não acho que (?) Os reflogs sejam pressionados, então isso funcionaria apenas em uma situação de repo único.
GaryO 8/09/10
-2

Acredito que encontrei uma maneira de lidar com todos os casos mencionados aqui:

branch=branch_A
merge=$(git rev-list --min-parents=2 --grep="Merge.*$branch" --all | tail -1)
git merge-base $merge^1 $merge^2

Charles Bailey está certo de que as soluções baseadas na ordem dos ancestrais têm apenas um valor limitado; no final do dia, você precisa de algum tipo de registro "esse commit veio do ramo X", mas esse registro já existe; por padrão, 'git merge' usaria uma mensagem de confirmação como "Merge branch 'branch_A' into master", isso informa que todas as confirmações do segundo pai (commit ^ 2) vieram de 'branch_A' e foram mescladas com o primeiro pai (confirmação ^ 1), que é 'mestre'.

Armado com essas informações, você pode encontrar a primeira mesclagem de 'branch_A' (que é quando o 'branch_A' realmente surgiu) e encontrar a base de mesclagem, que seria o ponto de ramificação :)

Eu tentei com os repositórios de Mark Booth e Charles Bailey e a solução funciona; como não pôde? A única maneira de isso não funcionar é se você tiver alterado manualmente a mensagem de confirmação padrão para mesclagens, para que as informações da filial sejam realmente perdidas.

Para utilidade:

[alias]
    branch-point = !sh -c 'merge=$(git rev-list --min-parents=2 --grep="Merge.*$1" --all | tail -1) && git merge-base $merge^1 $merge^2'

Então você pode fazer ' git branch-point branch_A'.

Desfrutar ;)

FelipeC
fonte
4
Confiar nas mensagens de mesclagem é mais frágil do que sugerir sobre a ordem dos pais. Também não é apenas uma situação hipotética; Costumo usar git merge -mpara dizer em que mesclei, em vez do nome de um ramo potencialmente efêmero (por exemplo, "mesclar alterações da linha principal no refator do recurso xyz"). Suponha que eu tenha sido menos útil com o meu -mno meu exemplo? O problema simplesmente não é solúvel em sua plena generalidade, porque eu posso fazer a mesma história com um ou dois ramos temporários e não há como diferenciar.
CB Bailey
1
@CharlesBailey Esse é o seu problema então. Você não deve remover essas linhas da mensagem de confirmação, adicione o restante da mensagem abaixo da original. Atualmente, o 'git merge' abre automaticamente um editor para você adicionar o que quiser, e nas versões antigas do git você pode fazer o 'git merge --edit'. De qualquer forma, você pode usar um gancho de confirmação para adicionar um "Confirmado no ramo 'foo'" a cada confirmação, se é isso que você realmente deseja. No entanto, esta solução funciona para a maioria das pessoas .
FelipeC
2
não funcionou para mim. O branch_A foi extraído do master, que já tinha muitas mesclagens. Essa lógica não me deu o hash de confirmação exato em que branch_A foi criado.
Venkat Kotra