Введение в Git/GitHub:
ошибки использования (ч. 9)

Введение

В предыдущей статье мы прошли базовые команды Git, которые применяются в повседневной работе. В этой публикации продолжим изучение данного инструмента, рассмотрев типичные ошибки при его использовании, а также способы их исправления.
Данная статья не ставит перед собой цель быть энциклопедией решений на все случаи жизни. Было бы неверным давать ответы на вопросы, которые на данном этапе изучения Git просто не могут возникнуть. Любая информация должна соответствовать вашему текущему уровню.

1. Самое простое решение многих проблем

Вы «натворили делов» со своим удаленным и локальным репозиториями (УР, ЛР), и вам кажется, что все сломалось и ничего не работает. В качестве максимально быстрого решения этой проблемы можно удалить в ЛР скрытую папку .git и репозиторий на GitHub, а затем начать все заново, выполнив git init. При этом ваши классы и папки удалять не нужно.
Удалить .git через консоль можно следующим образом:

> rm .git -rf
Либо, если у вас проблемы только с ЛР, то можете полностью удалить с вашего компьютера папку проекта (на всякий случай сделав ее копию), а затем клонировать УР с помощью git clone.
Когда я изучал Git, то десятки раз создавал и удалял свои репозитории, сталкиваясь с неразрешимыми для меня на тот момент проблемами.
Этот вариант имеет право на существование для начинающих разработчиков, т. к. они не обладают еще нужным набором знаний для применения более технологичных решений. Но, как вы понимаете, данный способ хорош до поры до времени.

2. Как изменить URL удаленного репозитория?

Вы неверно назвали свой репозиторий, например, вместо StartJava почему-то написали Lesson1 или JavaOps. На что наставник вам сказал, что его нужно переименовать.
Или вы удалили УР, а ЛР — оставили. После этого создали новый репозиторий на GitHub, изменив его первоначальное имя.
Например, URL репозитория выглядел так:

https://github.com/ichimax/lesson1.git
А после изменений ссылка стала следующей:

https://github.com/ichimax/startjava.git
При этом в настройках ЛР адрес старого репозитория остался прежним.
Для его обновления и вывода на консоль выполните следующее команды:

> git remote set-url origin новый_url.git
> git remote -v
Когда вам в дальнейшем потребуется выполнить push, то необходимо будет однократно написать полную версию git push -u origin master, чтобы заново связать ЛР с новым УР, а затем снова можете использовать сокращенный вариант в виде git push.

3. Как удалить файл из репозитория?

3.1. Удаление файла из индекса

Вы забыли занести в .gitignore правило для игнорирования class-файлов, что привело к добавлению их в индекс. Это очень частая ошибка у начинающих, которая влечет за собой попадание мусорных файлов в УР.
Смоделируем подобную ситуацию, сгенерировав class-файл:

> javac MyFirstApp.java
Внесем изменения в класс из предыдущих статей:

import java.util.Scanner;

public class MyFirstApp {
    public static void main(String[] args) {
        System.out.println("У какого языка программирования следующий слоган:");
        System.out.print("\"Написано однажды, ");
        System.out.println("работает везде!\"");

        String answer = new Scanner(System.in).next();
        if (answer.equalsIgnoreCase("Java")) {
            System.out.println("Вы угадали");
        } else {
            System.out.println("Увы, но - это Java");
        }
    }
}
Далее введем привычные команды:

> git add . && git status
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   MyFirstApp.class
        modified:   MyFirstApp.java
MyFirstApp.class в итоге попал в индекс Git. Необходимо его оттуда удалить, т. к. отслеживание изменений у данного вида файлов не несет никакой практической пользы.
Воспользуемся командой, подсказанной Git:

> git restore --staged MyFirstApp.class && git status
Changes to be committed:
        modified:   MyFirstApp.java

Untracked files:
        MyFirstApp.class
MyFirstApp.class снова стал неотслеживаемым. Но, что не менее важно — измененный класс MyFirstApp остался в индексе не тронутым. Мы не потеряли изменения, внесенные в него.
Чтобы обезопасить себя от подобных неприятностей в будущем, перед использованием команды git add, всегда просматривайте список файлов с помощью git status, которые вы планируете добавить в индекс. Также обязательно добавьте шаблон *.class в .gitignore, чтобы больше не спотыкаться о class-файлы.

3.2. Удаление файла из коммита

Теперь научимся удалять MyFirstApp.class из коммита.
Проверим состояние ЛР:

> git status

Changes to be committed:
        new file:   MyFirstApp.class
        modified:   MyFirstApp.java
Добавим все изменения в коммит, внесенные в предыдущей главе:

> git commit -m "Добавил quiz по слогану Java"
Но перед тем, как запушить коммит, посмотрим, какие файлы в него входят:

> git log -1 --name-only --oneline
ead5167 (HEAD -> master) Добавил quiz по слогану Java
src/MyFirstApp.class
src/MyFirstApp.java
С командной git log мы уже сталкивались в предыдущей статье. Из нового — аргумент --name-only, который позволяет вывести только имена измененных файлов, находящихся в коммите.
Из вывода log видно, что MyFirstApp.class оказался в коммите, и его нужно оттуда убрать.
Для этого пишем:

> git reset --soft HEAD~1
Разберем эту команду по частям:
  • reset — отменяет коммиты. В нашем случае коммит
    ead5167 (HEAD -> master) Добавил quiz по слогану Java
  • --soft — позволяет выполнить отмену коммита без потери изменений, внесенных в файлы. В нашем случае изменения в классе MyFirstApp
  • HEAD — указатель на ветку в которой вы находитесь. В нашем случае HEAD~1 указывает на первый коммит в дереве коммитов ветки master
Более подробно про команду reset (1, 2).
Т. к. HEAD указывает на ветку, в которой вы находитесь в данный момент, то это позволяет перемещаться по дереву коммитов и откатываться к любому из них.
Получается следующая картина: HEAD указывает на ветку master, а master является указателем на вершину в дереве коммитов.
Отобразим, куда указывает HEAD:

> git branch
* master
* обозначается текущая ветка.
Если команду ввести с параметром -v, то кроме названия ветки отобразится информация о коммите, на который указывает master:

> git branch -v
* master b59d871 Изменил вывод текста, отображаемого в консоль
После отмены коммита отобразим список оставшихся:

> git log --oneline
b59d871 (HEAD -> master, origin/master) Изменил вывод текста, отображаемого в консоль
39ba195 Переименовал about.txt в README.md и внес в него описание проекта
1e36e0f Инициализация проекта
Видим, что благодаря reset последний коммит из него был исключен.
При этом log отображает ту же самую информацию, о которой мы говорили выше: HEAD указывает на master, а он — на вершину дерева коммитов (коммит b59d871).
Команду reset необходимо использовать только для отмены коммитов, которые находятся исключительно на вашем компьютере и еще не попали на УР. Т. к. она меняет историю коммитов, то это может принести множество проблем для других людей, которые совместно с вами работают в одном репозитории, если вы выполните пуш после отмены коммита.
Посмотрим, в каком состоянии теперь находится ЛР:

> git status
Changes to be committed:
        new file:   MyFirstApp.class
        modified:   MyFirstApp.java
Что делать дальше, вы уже знаете: нужно убрать из индекса class-файл, и закоммитить java-класс.
Исключаем MyFirstApp.class:

> git restore --staged MyFirstApp.class
> git status
Changes to be committed:
        modified:   MyFirstApp.java

Untracked files:
        MyFirstApp.class
Осталось сделать коммит.
И тут вы понимаете, что забыли, как у вас назывался отмененный коммит: изменения в файле MyFirstApp.java остались все те же, значит, придумывать описание к коммиту нет смысла — нужно воспользоваться тем, которое было раньше.
Есть одна волшебная команда git reflog, которая отобразит все, что вы когда-либо сделали в своем репозитории с коммитами. Это своего рода лог всех ваших операций. Приведу его малую часть, которая нам необходима:

> git reflog
b59d871 (HEAD -> master, origin/master) HEAD@{0}: reset: moving to HEAD~1
ead5167 HEAD@{1}: commit: Добавил quiz по слогану Java
Из вывода видно описание отмененного коммита. Используем его для нового:

> git commit -m "Добавил quiz по слогану Java"
Больше подробностей о reflog по ссылке.
Не забудьте добавить *.class в.gitignore.

3.3. Удаление файла с GitHub

Самая неприятная ситуация — это когда мусорный файл был запушен на GitHub:
В этом случае наставник вам может написать замечание: «на GitHub не должно быть файлов с расширением *.class, только *.java. Необходимо удалить из УР все class-файлы».
Самый простой выход из этой ситуации — это удалить в ЛР class-файлы, добавить в .gitignore маску *.class и сделать новый пуш. После этих действий на GitHub class-файл удалится. Он также автоматически удалится и у всех людей, которые работают с вами в одном репозитории. Например, когда наставник перед проверкой вашего ДЗ сделает git pull, то удаленные вами class-файлы, удалятся автоматически и у него.
Ваши действия могут выглядеть примерно так:

D:\Java\StartJava (master -> origin)
> git rm src\*.class
rm 'src/MyFirstApp.class'

> git status
On branch master

Changes to be committed:
        new file:   .gitignore
        deleted:    src/MyFirstApp.class

> git commit -m "Добавил .gitignore с маской *.class"
[master 4bf0ddd] Добавил .gitignore с маской *.class
 2 files changed, 1 insertion(+)
 create mode 100644 .gitignore
 delete mode 100644 src/MyFirstApp.class

> git push

4. Как изменить описание коммита?

4.1. Коммит не был запушен

Вы сделали коммит, но еще его не запушили. Выясняется, что описание к нему содержит опечатку или это сообщение нужно изменить.
Разберем эту ситуацию, внеся ряд изменений в файл MyFirstApp.java. Добавим в него код, запрашивающий у участника квиза его имя, которое будет выводиться при правильном ответе:

import java.util.Scanner;

public class MyFirstApp {
    public static void main(String[] args) {
        Scanner console = new Scanner(System.in, "cp866");
        System.out.print("Введите, пожалуйста, свое имя: ");
        String name = console.next();

        System.out.println("У какого языка программирования следующий слоган:");
        System.out.print("\"Написано однажды, ");
        System.out.println("работает везде!\"");

        String answer = console.next();
        if (answer.equalsIgnoreCase("Java")) {
            System.out.println(name + ", вы угадали!");
        } else {
            System.out.println("Увы, но - это Java");
        }
    }
}
Выполним коммит:

> git add MyFirstApp.java
> git commit -m "Добавил ввод имени учасника"
Выясняется, что описание содержит опечатку в слове «учасника».
Для исправления воспользуемся двумя вариантами команды commit:
  • в первом случае при вводе git commit --amend, откроется редактор для изменения сообщения:
Такой способ удобен для исправления многострочных комментариев.
  • во втором случае команда ниже позволит внести изменение прямо из консоли:

> git commit --amend -m "Добавил ввод имени участника"
[master 96e2847] Добавил ввод имени участника
Убедимся, что описание к коммиту изменилось, а прежнее — не сохранилось, отобразив сокращенную информацию по последнему коммиту:

> git log --oneline -1
96e2847 (HEAD -> master) Добавил ввод имени участника
Данные способы позволяют внести изменение в описание только последнего коммита.

4.2. Коммит был запушен

4.2.1. Принудительная перезапись истории

Что описание к коммиту нужно изменить, вы вспомнили, когда он был уже отправлен на GitHub.
Чтобы решить эту задачу, необходимо к команде из предыдущего пункта добавить git push -f, которая принудительно перезапишет коммит с ошибочным описанием — исправленным:

> git commit --amend -m "Добавил ввод имени участника"
> git push -f
Среди прочего, в консоли отобразится примерно следующая строка:

+ 5aa3d6d...ce3cf6f master -> master (forced update).
Она сообщает, что было сделано принудительное обновление репозитория.
У ваших коллег (или наставника) после ввода git pull отобразятся строки, тоже сообщающие о принудительном изменении и последующем успешном слиянии (merge):

+ 5aa3d6d...ce3cf6f master     -> origin/master  (forced update)
Merge made by the 'ort' strategy.

5. Как выполнить пуш, если Git не дает это сделать?

Вы еще не до конца освоили Git и какое-то время вносили изменения в файлы прямо на GitHub. Но рано или поздно поняли, что для своего профессионального роста все же нужно изучить базовые команды Git, и начать применять их в консоли.
После этого у себя на компьютере начали писать код очередного домашнего задания, а затем решили его запушить на GitHub для проверки наставником. Но сделать пуш у вас в итоге не получилось, а в консоли отобразилась ошибка:

hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
Возможна и другая ситуация, когда вы пушите в один репозиторий на GitHub с разных компьютеров. Например, днем, в свободное от работы время, порешали домашку, а затем запушили ее на удаленный репозиторий. Вечером, придя домой, продолжили писать код для следующего ДЗ. А про код, запушенный на GitHub, либо забыли, либо подумали, что из-за него проблем не будет. Но в действительности оказалось все иначе.
Может быть и такое, что на GitHub случайно попали class-файлы, и вы без задней мысли пошли на сайт и удалили их.
Во всех этих ситуациях пуш приведет к одной и той же ошибке, связанной с тем, что истории изменений на локальном и удаленном репозитории пошли разными путями: на GitHub есть коммит, которого нет в ЛР. В дереве коммитов на ЛР на какое-то количество коммитов меньше, чем на УР.
Научимся решать эту проблему.

5.1. Подтягивание изменений с GitHub

Для моделирования ситуации я зашел в свой репозиторий на GitHub и внес в README.md следующие мелкие изменения:
  • добавил картинку в виде шапки
  • пункт «Командная строка» заменил на конкретное название используемой программы — cmder
  • добавил иконки к каждому пункту списка
В качестве описания к коммиту указал следующий текст:
При этом README.md содержит следующий код:

# [StartJava](https://topjava.ru/startjava) -- курс на Java для начинающих

![image](https://user-images.githubusercontent.com/29703461/194078652-25a6e509-cdc6-4af4-9ab0-78b6b336c749.png)

## Используемые на курсе инструменты и технологии

:coffee: Java

:octocat: Git/GitHub

:pager: cmder

:bookmark_tabs: Sublime Text

:fire: Intellij IDEA

:gem: SQL

:elephant: PostgreSQL

:newspaper: psql
Проделанные только что изменения я благополучно забыл подтянуть на свой компьютер с помощью git pull. И продолжил работать над классом MyFirstApp.java в ЛР, добавив в него еще один квиз и немного мелких правок.
В итоге класс (у меня на компьютере, не в GitHub) стал выглядеть так:

import java.util.Scanner;

public class MyFirstApp {
    public static void main(String[] args) {
        Scanner console = new Scanner(System.in, "cp866");
        System.out.print("Введите, пожалуйста, свое имя: ");
        String name = console.nextLine();

        System.out.println("\n1. У какого языка программирования следующий слоган:");
        System.out.print("\"Написано однажды, ");
        System.out.println("работает везде!\"");

        String answer = console.nextLine();
        if (answer.equalsIgnoreCase("Java")) {
            System.out.println(name + ", вы угадали!");
        } else {
            System.out.println("Увы, но - это Java");
        }

        System.out.println("\n2. Какая фамилия у автора языка Java?");

        answer = console.nextLine();
        if (answer.equals("Гослинг") || answer.equals("Gosling")) {
            System.out.println(name + ", вы угадали!");
        } else {
            System.out.println("Увы, но - это Гослинг (Gosling)");
        }
    }
}
Добавим изменения в коммит:

> git add MyFirstApp.java
> git commit -m "Добавил quiz по автору Java"
Отобразим полученный результат в консоли:

> git log --oneline -1
7106587 (HEAD -> master) Добавил quiz по автору Java
А теперь самое интересное: попробуем сделать push:

> git push
To https://github.com/ichimax/startjava2.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://github.com/ichimax/startjava2.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Отобразилась ошибка, означающая, что в локальном и удаленном репозитории истории изменений пошли разными путями и не соответствуют друг другу.
Для решения этой проблемы Git предлагает выполнить команду pull, которая подтянет коммиты с УР, а затем объединит их с локальными, чтобы выровнять историю:

> git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 1.16 KiB | 62.00 KiB/s, done.
From https://github.com/ichimax/startjava2
   ce3cf6f..2bdecf7  master     -> origin/master
Merge made by the 'ort' strategy.
 README.md | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)
В этом сообщении стоить обратить внимание на строку Merge made by the 'ort' strategy. Из нее мы можем сделать вывод, что произошло слияние (merge).
Слияние — это объединение истории коммитов из разных веток в одну.
У нас ветка хоть и одна (master), но история коммитов на GitHub и в ЛР стала с какого-то момента различаться. Из-за этого и не удавалось сделать push. Когда мы выполнили git pull, то Git автоматически объединил (что бывает не всегда) последний коммит из УР с последним коммитом из ЛР, создав новый.
Не всегда Git может выполнить слияние автоматически, что вызывает конфликт слияния. Такое бывает, когда в разных ветках был изменен один и тот же фрагмент кода. Из-за этого Git не сможет определить, какую версию использовать. Для решения проблемы потребуется вмешательство пользователя. Более подробно об этом можно узнать по ссылке.
Для того, чтобы минимизировать возникновение подобных конфликтов, рекомендуется каждый раз перед началом работы с проектом делать git pull, чтобы синхронизировать историю коммитов ЛР с историей УР.
Отобразим 4 последних коммита:

> git log --oneline -4
1b4a7b7 (HEAD -> master) Merge branch 'master' of https://github.com/ichimax/startjava2
7106587 Добавил quiz по автору Java
2bdecf7 (origin/master) Обновил README.md
ce3cf6f Добавил ввод имени участника
Из этого списка видно, что коммит:
2bdecf7 — был сделан на GitHub
7106587 — был сделан в ЛР
1b4a7b7 — получился автоматически после слияния двух предыдущих коммитов
Обратите внимание, что у коммита, сделанного на GitHub, ветка называется не master, а origin/master. Разница в том, что master — это имя локальной ветки, а origin/master является удаленной веткой с псевдонимом origin (для адреса УР) и именем главной ветки — master.
Отобразим ветки, связанные с нашим репозиторием:

> git branch -a
* master
  remotes/origin/master
То, что master и origin/master — разные ветки, можно убедиться визуально, введя следующую команду:

> git log --pretty=format:"%h - %s" --graph
*   1b4a7b7 - Merge branch 'master' of https://github.com/ichimax/startjava2
|\ 
| * 2bdecf7 - Обновил README.md
* | 7106587 - Добавил quiz по автору Java
|/
* ce3cf6f - Добавил ввод имени участника
* 4bf0ddd - Добавил .gitignore с маской *.class
* 90ca67c - Добавил quiz по слогану Java
* b59d871 - Изменил вывод текста, отображаемого в консоль
* 39ba195 - Переименовал about.txt в README.md и внес в него описание проекта
* 1e36e0f - Инициализация проекта
Опция --graph позволяет вывести граф в формате ASCII, который показывает текущую ветку и историю слияний. Более подробно с разными опциями команды log можно ознакомиться по ссылке.
Давайте взглянем на различия в последних коммитах в удаленной и локальной master-ветках:

D:\Java\StartJava\src (master -> origin)
> git diff origin/master..master
diff --git a/src/MyFirstApp.java b/src/MyFirstApp.java
index 8e25560..08099ad 100644
--- a/src/MyFirstApp.java
+++ b/src/MyFirstApp.java
@@ -4,15 +4,22 @@ public class MyFirstApp {
     public static void main(String[] args) {
         Scanner console = new Scanner(System.in, "cp866");
         System.out.print("Введите, пожалуйста, свое имя: ");
-        String name = console.next();
+        String name = console.nextLine();

-        System.out.println("У какого языка программирования следующий слоган:");
+       System.out.println("\n1. У какого языка программирования следующий слоган:");
         System.out.print("\"Написано однажды, ");
         System.out.println("работает везде!\"");

-        String answer = console.next();
+        String answer = console.nextLine();
         if (answer.equalsIgnoreCase("Java")) {
             System.out.println(name + ", вы угадали!");
         } else { 
            System.out.println("Увы, но - это Java");
         }
+
+        System.out.println("\n2. Какая фамилия у автора языка Java?");
+
+        answer = console.nextLine();
+        if (answer.equals("Гослинг") || answer.equals("Gosling")) {
+            System.out.println(name + ", вы угадали!");
+        } else {
+            System.out.println("Увы, но - это Гослинг (Gosling)");
+        }
     }
 }
Как вы, наверняка, уже знаете, плюсами обозначают добавленные (новые) строки, а минусами — удаленные.
После всех манипуляций отобразим состояние репозитория:

> git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)
 
nothing to commit, working tree clean
В сообщении говорится, что локальная ветка master содержит на два коммита больше, чем origin/master в УР. Посмотрим, что это за коммиты:

> git log --branches --not --remotes --oneline
1b4a7b7 (HEAD -> master) Merge branch 'master' of https://github.com/ichimax/startjava2
7106587 Добавил quiz по автору Java
Выполним push, отправив эти два коммита на УР, и введем команду, отображающую список из последних 4 коммитов:

> git push
> git log --oneline -4
1b4a7b7 (HEAD -> master, origin/master) Merge branch 'master' of https://github.com/ichimax/startjava2
7106587 Добавил quiz по автору Java
2bdecf7 Обновил README.md
ce3cf6f Добавил ввод имени участника
По строке 1b4a7b7 (HEAD -> master, origin/master) видно, что история коммитов выровнялась: HEAD в ЛР и УР указывают на самый последний коммит 1b4a7b7.

Заключение

В статье мы рассмотрели самые простые, но часто возникающие проблемы, которые могут появиться при работе с Git и GitHub. Это базовый минимум, который поможет вам на первых порах не чувствовать себя растерянным, столкнувшись лицом к лицу с описанными сложностями.
Ниже в таблице перечислены ключевые команды статьи.
Автор: Чимаев Максим
Оцените статью, если она вам понравилась!