1 pontos por GN⁺ 2 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • O comando experimental git history, introduzido entre o Git 2.54 e 2.55, automatiza a modificação de commits antigos e a reorganização de branches derivadas sem alterar o fluxo de trabalho tradicional do Git
  • fixup incorpora alterações já preparadas em um commit antigo e depois faz rebase automaticamente de todos os branches locais derivados desse commit para o novo hash
  • reword altera a mensagem de um commit antigo, e split divide as mudanças de um commit em dois commits no nível de hunk, reorganizando em seguida os commits e branches posteriores
  • Os três subcomandos usam processamento atômico, recusando operações com potencial de conflito para evitar que a árvore de trabalho fique quebrada no meio do processo, mas não funcionam em históricos com commits de merge
  • Embora não ofereça o tratamento de conflitos como entidade de primeira classe no jj, desfazer baseado em log de operações nem a modelagem da working copy como commit, ele permite usar vários recursos de edição de histórico no estilo do jj diretamente na distribuição principal do Git, sem instalação separada

Por que editar histórico no Git tradicional é incômodo

  • Ao lidar com várias mudanças em paralelo, é preciso alternar constantemente entre branches e commits, e durante um rebase -i a árvore de trabalho pode acabar em um estado incompleto
  • O substituto do Git jj é frequentemente citado como solução para esse problema, mas mesmo tentando usá-lo por alguns dias a cada três meses ao longo de um ano e meio, no fim a pessoa sempre acaba voltando ao fluxo tradicional do Git
  • git history é um comando experimental incluído na distribuição principal do Git, então pode ser testado sem instalar ferramentas extras
  • Os subcomandos são três: fixup, reword e split

fixup: corrigir ao mesmo tempo um commit antigo e branches derivados

  • git history fixup corrige partes erradas de um commit antigo e faz rebase automático dos branches derivados desse commit
  • Basta preparar as alterações com git add, como em um processo comum de correção, e então executar git history fixup <commit> para incorporar as mudanças preparadas ao commit alvo
  • É parecido com combinar git commit --fixup com rebase autosquash, mas com a diferença de que também atualiza outros branches que contêm o commit alvo
  • Enquanto git rebase --update-refs move apenas referências dentro do escopo do rebase atual, git history encontra e reescreve todos os branches locais derivados do commit alvo
    • Se necessário, é possível limitar o escopo da atualização apenas ao branch atual
    • Não funciona em históricos com commits de merge, então pode ser difícil aplicá-lo a alguns modos de uso do Git
  • Ao incorporar uma correção em B, é criado um novo B* com hash diferente, e os commits acima dele, C e D, também são recriados como C* e D*
    • As pontas dos branches feat-1 e feat-2 também passam a apontar para os novos commits

Processamento atômico e restrição a conflitos

  • Os três subcomandos são todos processados de forma atômica, sem deixar a árvore de trabalho em um estado parcialmente quebrado
  • A atomicidade é garantida recusando a própria operação quando ela pode causar conflitos
  • Isso é mais limitado funcionalmente do que o jj, que trata conflitos como objetos de primeira classe, permitindo manter o estado de conflito durante o rebase e resolvê-lo depois
  • As limitações do git history foram aplicadas de forma intencional para que a reescrita de histórico não se transforme em uma operação de preservação de estado
    • Se o Git passar a oferecer suporte a conflitos de primeira classe no futuro, essa limitação poderá ser removida

reword: mudar a mensagem de commit sem mexer na árvore de trabalho

  • git history reword altera a mensagem de um commit antigo e reorganiza automaticamente os commits e branches acima dele
  • Ao executar git history reword <commit>, o editor abre com a mensagem atual; depois de editar e salvar, a pilha de commits posteriores é reconstruída com os novos hashes
  • É útil para ajustar mensagens de commits anteriores à estrutura atual quando o design muda durante o desenvolvimento iterativo
  • Funciona da mesma forma que fixup, exceto pelo fato de alterar apenas a mensagem do commit, e não o conteúdo da árvore
  • reword não mexe no índice nem na árvore de trabalho; ele altera apenas o grafo de commits
    • Até commits de branches que não estão em checkout podem ser modificados sem atrapalhar o trabalho atual
    • Se a mensagem de B for alterada, o hash muda, então o commit seguinte C é recriado como C* e feat-1 também passa a seguir o novo commit

split: dividir um commit em dois

  • git history split permite selecionar interativamente as mudanças de um commit para separá-las em dois commits
  • Ao executar git history split <commit>, abre-se um prompt para escolher, no nível de hunk, quais mudanças do commit alvo serão mantidas no primeiro commit
    • Os hunks selecionados e mantidos formam o primeiro commit
    • Os hunks restantes entram no segundo commit
  • É parecido com git add -p, mas sem exigir o procedimento complexo de combinar isso com git rebase
  • Se um B que continha duas mudanças sem relação for dividido, surgem B1 e B2, e o commit posterior C é reconstruído como C* sobre esses dois commits
  • Entre os três comandos, este é o mais específico em termos de uso, mas é útil quando realmente é preciso separar um commit

Diferenças de funcionalidade em relação ao jj

  • git history não substitui todo o modelo de trabalho oferecido pelo jj
  • Em contrapartida, git history oferece, dentro do Git tradicional, edição de commits antigos, mudança de mensagens, divisão de commits e atualização de branches derivados, permitindo trazer algumas vantagens do jj para o fluxo de trabalho atual
  • Como ainda existe a possibilidade de afrouxar essas limitações no futuro, há espaço para melhorias adicionais em lançamentos futuros do Git

1 comentários

 
GN⁺ 2 시간 전
Comentários do Hacker News
  • O Git era incômodo, mas melhorou depois de ler os três primeiros capítulos do livro Pro Git, disponibilizado gratuitamente. Ele oferece um excelente modelo mental sobre o funcionamento interno do Git e, para o bem ou para o mal, a interface do usuário do Git reflete diretamente sua estrutura interna, então, depois de entender isso, tudo passou a fazer sentido

    • Não concordo que a interface do usuário reflita diretamente a estrutura interna. Por exemplo, git checkout não perde alterações ainda não adicionadas ao Git ao trocar de branch, mas foi projetado para reverter mudanças ao restaurar outra versão de um arquivo; como isso refletiria diretamente a estrutura interna?
      Além disso, a inconsistência da CLI, que dificulta encontrar a sintaxe exata da linha de comando mesmo quando você sabe a operação e o comando desejados internamente, também torna o Git difícil de usar
    • O Git não é tão confuso quanto dizem; basta dedicar algumas horas para entendê-lo. É uma ferramenta essencial para engenheiros de software, e é uma pena não aprendê-la; se você é profissional, git commit -am “Changes” sozinho não basta
    • Eu também achava os comandos do Git confusos, mas depois de ver um vídeo no YouTube em que o Linus explica as estruturas de dados internas do Git, percebi como ele é bem simples. Ficou fácil associar como as funções da ferramenta manipulam as estruturas de dados e, na verdade, as abstrações de nível mais alto que o Git tenta oferecer deixam a situação mais confusa
    • Não é bem assim. Metade dos comandos do Git é de baixo nível e a outra metade é de alto nível, então eles não têm correspondência 1:1 com a estrutura interna
    • A experiência de uso do Mercurial era de longe a melhor, e dava para aprender sem nem ler documentação. Mesmo usando Git, passei a carreira inteira tentando usá-lo como se fosse Mercurial e, quando algo dava errado, apagava o repositório local
  • Existe git rebase --abort, e também dá para criar uma tag antes do rebase e depois voltar com git reset --hard $set_tag. Como o estado anterior não desaparece, não há motivo para ter medo de rebase interativo

    • Eu costumo criar várias branches chamadas temp ou before-rebase exatamente para isso. Uso como se fossem tags, mas branches são mais fáceis de mover, então antes de um rebase -i grande normalmente executo git branch before-rebase/some-feature
      Na prática, quase nunca precisei de git reset before-rebase, mas muitas vezes comparei git log -p before-rebase com a branch depois do rebase para verificar alterações não intencionais no processo de resolução de conflitos
    • Nem é preciso criar uma tag de segurança separada; o reflog já faz esse papel automaticamente
    • Você usa git reflog?
    • Não tem como ser mais simples que jj undo
    • Exato. Metade das vezes em que digito git rebase, logo em seguida digito --abort
  • Eu não me vejo como coder ou programador, mas aprender Git me deu algo como um superpoder de organização na forma de pensar. Uso para uma enorme variedade de coisas: sites, projetos de design, eletrônica, composição musical, repositório pessoal de conhecimento, scripts de administração remota, gerenciamento de configuração, trechos de código etc. Não é perfeito, mas recomendo que todo mundo com quem trabalho aprenda

    • Minha capacidade de organização também melhorou muito, mas isso tem mais a ver com o efeito dos sistemas de controle de versão em geral do que com o Git em si. Um sistema de controle de versão organiza e preserva o que você criou e permite recuperar quantas cópias quiser a qualquer momento, o que ajuda a trabalhar sem preocupação
      Em Getting Things Done, de David Allen, há a ideia de que você precisa de um sistema confiável para reunir e organizar seus pensamentos. Eu uso OmniFocus para tarefas, projetos e listas, mas confio no Git para organizar e preservar software e alguns dados que crio no computador
    • Como você usa Git para composição musical?
  • Em teoria eu gosto muito desses comandos e também uso sempre os equivalentes no jj, mas não consegui descobrir como assinar os commits modificados por git history. Mesmo olhando o manual e o código-fonte do Git, se eu modifico commits já assinados com GPG usando git history reword HEAD~, todas as assinaturas desaparecem
    Como eu queria manter as assinaturas dos commits, no fim voltei ao tradicional git rebase -i

    • Isso parece uma omissão importante e também parece contornar o fluxo padrão de tratamento de commits. O manual também diz que git history é um recurso experimental, então vale a pena reportar como bug
  • Mesmo sem comandos novos, venho fazendo grandes alterações no histórico local de commits do Git há mais de 10 anos sem muita dificuldade. Tenho convicção de que um dos principais motivos de Git e rebase parecerem difíceis é um problema de interface do usuário: precisar descobrir por conta própria as opções exatas da linha de comando e como elas funcionam
    No Windows, a caixa de diálogo de rebase do TortoiseGit era poderosa e ao mesmo tempo fácil de usar. Bastava clicar com o botão direito no commit de base no log de commits e escolher "Rebase onto this", e, como em https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm..., ficava claro como reordenar, dividir, combinar e editar commits; se cancelasse no meio, tudo voltava ao estado original
    Porém, como ele usa uma extensão de shell, há cliques demais; eu preferiria mais se fosse um app independente com abas, embora isso pareça vir dos princípios de design herdados do TortoiseSVN. Também testei o Git Extensions por um tempo, mas não encontrei recursos que usava com frequência, ou já estava acostumado ao TortoiseGit e acabei não migrando. A tela de log pode ser vista em https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht...

    • Uma das coisas de que senti falta ao migrar do Windows para o Linux foi o shell integrado, em que qualquer programa podia adicionar menus de clique direito para arquivos e pastas, overlays como os ícones de status do TortoiseGit e objetos especiais como My Computer. Pelo que entendo, isso atingiu o auge no XP, e a partir do Vista a Microsoft mudou a direção para algo mais difícil de estender
      Ainda assim, até hoje não entendo por que algum menu do VC++6 criava um objeto de shell parecido com uma pasta em vez de uma lista comum, e o mostrava na caixa de diálogo de abrir arquivo. Era um assistente para adicionar componentes ao projeto, e eu às vezes ia procurar aquela pasta falsa em My Computer enquanto a caixa de diálogo estava aberta
    • É verdade que o fluxo de trabalho anterior era um pouco mais trabalhoso e que os novos comandos são mais convenientes. fixup já era possível com git commit --fixup ou, antes disso, com git rebase -i, e reword também dava para fazer com git rebase -i
      Parece que se acumulou feedback suficiente de que era incômodo editar um arquivo para decidir a ação de cada commit, então foi adicionado um comando de nível mais alto que encapsula a funcionalidade existente
    • Entre as interfaces de usuário para Git, nunca vi nada que chegasse perto do Magit. Ele oferece ao mesmo tempo recursos profundos e uma interface simplificada que preserva estado
      Ferramentas de linha de comando dificultam o acesso à saída anterior quando você executa o próximo comando. Mesmo que eu queira fazer cherry-pick de um commit visto em git log, se o hash já não estiver mais visível no terminal, não dá para digitá-lo só de memória; já uma interface com estado consegue manter várias interações em paralelo e preservar informações anteriores
      O Git tem muita informação de estado para lembrar: branch atual, branch anterior, arquivos modificados, commits ainda não enviados por push etc. Se você não dominar ferramentas ao redor, como exibição de branch em plugins de shell e autocompletar, várias abas e divisões de janela, cópia entre abas e busca no autocompletar, usar Git pode se tornar muito desagradável
  • Vejo a fraqueza prática do jj basicamente só na integração com Git. Ele formaliza linguagens específicas de domínio para conjuntos de arquivos, conjuntos de revisões e templates, usando isso de forma consistente em toda a configuração e na CLI, e fornece conceitos poderosos como interface sem expor diretamente a estrutura interna
    Mas ele é flexível demais, então a interoperabilidade com Git depende bastante de convenções e disciplina, e como quase todo estado do jj pode ser expresso em Git, é fácil bagunçar o repositório remoto. Para recuperar isso, é preciso entender Git em profundidade
    Para um novo projeto solo, vale a pena tentar jj. Em condições favoráveis, ele passa uma sensação parecida com o Git inicial comparado ao Subversion, e, se você primeiro se acostumar com a forma de trabalhar do próprio jj, talvez fique um pouco mais preparado para a difícil integração posterior com Git. O contexto relacionado está em https://news.ycombinator.com/item?id=48903054

    • Em princípio, o jj também tem backends que não são Git, mas no momento o único explicitamente indicado como adequado para ambiente operacional é o backend Git. Um jj sem integração com Git ainda está mais para conceito do que para realidade
      Gosto do jj e do seu modelo mental de controle de versão, então vou continuar usando, mas às vezes preciso descer para o Git para concluir o trabalho, então ele parece mais um wrapper conveniente sobre o Git do que um novo sistema de gerenciamento de configuração. Se o Git adicionar comandos que deem suporte ao fluxo de trabalho do jj, será difícil encontrar motivo para instalar o jj separadamente
  • A forma como git history fixup faz rebase automático de todos os branches descendentes não combina com meu fluxo de trabalho. Eu preservo cada versão de um branch de funcionalidade como myfeature.1, myfeature.2 etc. para encontrar versões antigas quando o comportamento muda em uma versão nova, e git reflog não substitui isso
    Por exemplo, se o problema surgiu entre myfeature.58 e myfeature.59 e cada funcionalidade tem 15 commits, com git range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59 dá para ver as diferenças entre as duas versões commit a commit, mesmo que os commits de base sejam diferentes. Por isso, não quero rebase automático de todos os branches que contenham um determinado commit, e a abordagem de usar tags para versões antigas também não funcionou bem para mim

  • O Git recente implementou, como comandos separados com menos atrito, três tarefas comuns de git rebase --interactive, e isso parece funcionar apenas quando não há conflitos

    • Tenho dúvidas se git history é realmente útil. Eu prefiro mover commits visualmente com rebase interativo, e não gostaria de acabar apontando por engano um hash de commit órfão que sumiu do branch local atual como alvo de git history
  • Não entendo por que dedicar tanto esforço a deixar o histórico do Git perfeitamente arrumado. No fim, ninguém relê commits individuais, então basta fazer squash de tudo antes do merge

    • Discordo fortemente. Ao revisar um PR, eu pelo menos leio o histórico de commits para entender o fluxo das mudanças, e se ele estiver cheio de "oops" e "fix", eu rejeito na hora.
      Os commits contam a história do trabalho, então é preciso oferecer um histórico de commits legível para que o revisor não precise se esforçar ainda mais para entender a intenção
    • Eu leio commits individuais com frequência. Em repositórios cuidadosamente mantidos, isso é muito útil para rastrear bugs ou regressões e entender a intenção por trás da mudança do autor
    • Quando quero saber por que o código ficou desse jeito, ajuda muito olhar o contexto do commit que introduziu aquele código. Se for um commit gigante contendo várias mudanças diferentes de uma vez, o valor disso cai bastante
    • Esse é o tipo de opinião que mostra que você não trabalha em uma equipe grande. Quando volto de férias, a primeira coisa que faço é ler o histórico do Git para ver o que mudou nesse período, e isso também é útil quando o CI quebra, então na prática eu olho o histórico de alguma forma quase todos os dias.
      Se a base de código e as funcionalidades forem bem pequenas, talvez dê para fazer squash de tudo, mas eu prefiro uma forma que permita verificar o fluxo real das mudanças e fazer cherry-pick ou reverter commits seletivamente
    • Um histórico bem organizado, com um commit por mudança lógica, é extremamente útil ao fazer busca binária em regressões
  • Entendi que, com D atualmente em checkout, ao mudar B para B', o git rebase --update-refs deixaria E fora do intervalo do rebase como está e criaria apenas B' → C' → D', mantendo o B → E existente.
    Já o git history fixup atualizaria até E, gerando E'? Se sim, queria saber se também existe alguma forma de fazer a atualização de todos os branches descendentes usando o git rebase, ao qual estou acostumado há décadas

    • O hash de um commit também reflete o hash do commit pai, então não dá para manter E como está. Nunca usei --update-refs diretamente, mas, se a documentação estiver correta, o resultado deve ser o terceiro grafo, gerando até E', então o comportamento desejado já é possível com git rebase
    • Consulte https://blog.hot-coffee.dev/en/blog/git_update_refs/