Managing files on GitHub¶
Sam is away from home, with only a browser, and wants to add a backup script to the homelab. GitHub can create, upload, edit, move, and delete files directly on the website. Each of these actions creates a commit, exactly as git commit would.
sequenceDiagram
participant B as Browser (github.com)
participant G as GitHub repository
participant L as Local clone
B->>G: Create, upload, edit or delete a file = 1 commit
B->>G: Another change = another commit
L->>G: git pull
G-->>L: The commits made in the browser
Creating a file¶
On the repository page (Code tab), click Add file > Create new file.
- Type the file name in the Name your file... field, next to the repository name.
- Write the content in the Edit tab. The Preview tab renders Markdown files.
- Click Commit changes....
Creating a directory¶
GitHub has no "new folder" button: type a / in the file name. Typing scripts/ turns scripts into a directory segment of the path, and the field moves on to the file name: scripts/backup.py creates the scripts directory and the file inside it. Press backspace at the start of the field to edit the directory part again.
Git tracks files, not directories: an empty directory cannot be committed. To keep an empty directory in a repository, the convention is to put an empty file named .gitkeep in it.
The commit dialog¶
Every change in the browser ends with the same dialog:
| Field | Purpose |
|---|---|
| Commit message | The first line of the commit message. GitHub suggests one (Create backup.py, Update README.md...): replace it with a meaningful message |
| Extended description | Optional. The body of the commit message: the why of the change |
Commit directly to the main branch |
The commit is added to the current branch |
| Create a new branch for this commit and start a pull request | The commit goes to a new branch (GitHub suggests <username>-patch-1), and GitHub opens a pull request form. The button becomes Propose changes |
Sam commits the backup script:
| Field | Value |
|---|---|
| Commit message | Add NAS backup script |
| Extended description | Copies the Samba shares to the USB disk every night. |
| Option | Commit directly to the main branch |
Committing directly to main is fine for a personal project. In a team, or when main is protected, choose the new branch option: see Branches and branch protection and Pull requests.
Uploading files¶
Add file > Upload files adds files from your computer:
- Drag and drop files or folders onto the page, or click choose your files.
- Fill in the commit dialog. The default message is
Add files via upload. - Click Commit changes.
All the uploaded files go into a single commit: it is the only way to commit several files at once in the basic web interface. To upload into a directory, first open that directory in the file list, then use Add file.
| Limit | Value |
|---|---|
| Files per upload | 100 |
| Size of a file uploaded in the browser | 25 MiB |
| Size of a file pushed with Git | 100 MiB (with a warning from 50 MiB) |
| Larger files | Git Large File Storage (Git LFS) |
Editing, renaming, and moving a file¶
Open the file, then click the pencil icon (Edit this file). The editor is the same as for a new file:
- Edit the content, and use the Preview tab to check the result, including the changes for files that are not Markdown.
- Rename the file by changing its name in the name field.
- Move it by editing the path in the name field: add
docs/to move it into a directory, or press backspace at the start of the field to move it up one level.
Then commit. The default message describes the action: Update inventory.csv, Rename notes.txt to services.md. A rename or move is recorded by Git as a rename, with the history intact:
$ git show --stat --format=%s HEAD
Rename notes.txt to services.md
notes.txt => services.md | 0
1 file changed, 0 insertions(+), 0 deletions(-)
Other buttons on a file page give access to the same information as Git commands: Raw (the bare file content), Blame (who last changed each line, like git blame), History (the commits that changed this file, like git log <file>), and the copy and download icons.
Deleting a file¶
Open the file, then click the ... menu (top right of the file) > Delete file. GitHub shows the deletion as a diff; commit it (default message Delete <file>). A directory can be deleted the same way from its own page, with Delete directory.
Deleting a file removes it from the latest version, not from the history. Any earlier commit still contains it, exactly as with git rm.
A deleted secret is still public
Sam once uploaded a .env file with the NAS password, then deleted it with a second commit. The file is gone from the repository page, but anyone with a clone can still read it:
$ git log --oneline -- .env
aff705a Delete .env
9d32749 Add files via upload
$ git show 9d32749:.env
NAS_PASSWORD=hunter2
The only real fix is to change the password (or revoke the token). Rewriting the history to remove the file is possible, but complex, and does not help against copies made in the meantime. Prevent the problem instead: list secret files in .gitignore, and check what you upload. On public repositories, GitHub's secret scanning and push protection detect many kinds of tokens and can block them.
The result, seen from Git¶
Every action in the browser is a commit like any other. After Sam's session, a git pull on the desktop brings back:
$ git log --oneline --stat
aff705a Delete .env
.env | 1 -
1 file changed, 1 deletion(-)
9d32749 Add files via upload
.env | 1 +
1 file changed, 1 insertion(+)
f9e2d87 Add NAS backup script
scripts/backup.py | 2 ++
1 file changed, 2 insertions(+)
0963cd0 Create inventory.csv
inventory.csv | 2 ++
1 file changed, 2 insertions(+)
2203dfe Initial commit
README.md | 1 +
1 file changed, 1 insertion(+)
Browser and local clone: one more copy to keep in sync
A commit made in the browser is a commit the local clones do not have yet. Pull before working locally, or the next push is rejected, exactly as in Pushing to a remote.
Browser or Git?¶
| Task | Browser | Local Git |
|---|---|---|
| Fix a typo, edit one file | Quick and easy | Clone, edit, commit, push |
| Change several files in one commit | Only through an upload, or github.dev (. key) |
Natural |
| Test a change before committing (run a script...) | Impossible | Natural |
| Work offline | Impossible | Natural |
| Large or many files | Limited (25 MiB, 100 files) | Up to 100 MiB per file, or Git LFS |
Summary¶
| Action | Where | Default commit message |
|---|---|---|
| Create a file | Add file > Create new file | Create <file> |
| Create a directory | Type / in the file name |
Create <file> |
| Upload files | Add file > Upload files | Add files via upload |
| Edit a file | Pencil icon on the file | Update <file> |
| Rename or move a file | Name field in the editor | Rename <old> to <new> |
| Delete a file | ... > Delete file | Delete <file> |
Common mistakes¶
- Keeping the default commit message.
Update README.mdsays nothing about what changed, or why. - Trying to create an empty directory. Git only tracks files: add a
.gitkeep. - Deleting a committed secret and considering it safe. It stays in the history: change it.
- Forgetting to pull after working in the browser. The next local push is rejected.
Hands-on labs¶
Three labs, from guided to more autonomous. They need your GitHub account and Git. Replace <username> with your GitHub username.
Lab 1: build a repository from the browser¶
Objective: create, upload, edit, and delete files on GitHub, then find each action as a commit in a local clone.
Prerequisites and initial state: a GitHub account, Git installed. The setup creates a local services.md to upload.
Setup: on GitHub, create a public repository homelab-practice-3 with a README. Then:
mkdir -p ~/git-practice/web-files && cd ~/git-practice/web-files
printf '# Services\n\n- pi-dns: Pi-hole\n- nas: Samba shares\n' > services.md
Tasks:
- Create
inventory.csvcontaining a header linehostname,ip,roleand the linepi-dns,192.168.1.10,dns. Commit with the messageCreate inventory. - Create
scripts/backup.py, containingprint("backup"), with the messageAdd NAS backup scriptand the extended descriptionCopies the Samba shares every night. - Upload
~/git-practice/web-files/services.md, with the messageAdd services list. - Edit
inventory.csvto addnas,192.168.1.20,storage. Check the change in the preview, then commit with the messageAdd NAS. - Delete
scripts/backup.py, with the messageRemove backup script. Look at what happened to thescriptsdirectory. - In the terminal, clone the repository into
~/git-practice/web-files, and display the history with the files changed by each commit. Then display the full message ofAdd NAS backup script.
Expected result and verification:
- After task 5, the
scriptsdirectory has disappeared from the repository page: it contained no other file. git log --oneline --statshows 6 commits, fromInitial committoRemove backup script, each with the file it changed.- The full message of
Add NAS backup scriptincludes the extended description as its body.
Solution
cd ~/git-practice/web-files
git clone https://github.com/<username>/homelab-practice-3.git
cd homelab-practice-3
git log --oneline --stat
git log -1 --format=%B ":/Add NAS backup script"
:/Add NAS backup scriptdesignates the latest commit whose message contains this text: handy when you do not know the hash.- The directory disappears with its last file: Git has no notion of an empty directory.
- Each browser action made exactly one commit; the upload would also have been a single commit with several files.
Keep homelab-practice-3 for Lab 2. Clean up the clone: rm -rf ~/git-practice/web-files.
Lab 2: the secret that would not go away¶
Objective: make the classic mistake on purpose, uploading a secret, and prove that deleting it does not remove it from the history.
Prerequisites and initial state: homelab-practice-3 from Lab 1 (or any practice repository of yours). The setup creates a fake secret file to upload. Use a fake value only.
Setup:
mkdir -p ~/git-practice/secret && cd ~/git-practice/secret
echo "NAS_PASSWORD=not-a-real-password" > .env
Tasks:
- Upload
~/git-practice/secret/.envtohomelab-practice-3(your file manager may hide dot files: drag the file from a terminal-opened folder, or use choose your files and show hidden files). Keep the default commit message. - Delete
.envon GitHub, with the messageDelete .env. - Clone the repository into
~/git-practice/secret. Check that.envis not in the working directory. - With Git, find the commits that touched
.env, and display its content in the commit that added it. - On GitHub, find the same information with the History of the repository (N commits link), by opening the upload commit.
- Prevent it from happening again: create a
.gitignorecontaining.envon GitHub. Locally, pull, recreate.env, and check that Git ignores it.
Expected result and verification:
- Task 3:
ls -ashows no.env. - Task 4:
git log --oneline -- .envlistsDelete .envandAdd files via upload;git show <hash>:.envprintsNAS_PASSWORD=not-a-real-password. - Task 6:
git check-ignore -v .envprints.gitignore:1:.env .env, andgit statusshows nothing to commit.
Solution
# 3. Not in the latest version...
cd ~/git-practice/secret
git clone https://github.com/<username>/homelab-practice-3.git
cd homelab-practice-3
ls -a
# 4. ...but still in the history
git log --oneline -- .env
git show <hash>:.env # the hash of "Add files via upload"
# 6. After creating .gitignore on GitHub
git pull
echo "NAS_PASSWORD=not-a-real-password" > .env
git check-ignore -v .env # .gitignore:1:.env .env
git status # nothing to commit, working tree clean
- With a real password, the only safe action after task 1 is to change it.
git log -- <file>also works for files that no longer exist, as long as you put--before the name.- The
.gitignoreprotects uploads made with Git, not uploads made in the browser: GitHub accepts any file you upload by hand.
Clean up when you are done: rm -rf ~/git-practice/secret, and delete homelab-practice-3 on GitHub.
Lab 3: reorganize a repository from the browser¶
Objective: restructure a repository using only the web editor, then check from Git that the moves were recorded as renames.
Prerequisites and initial state: a GitHub account, Git installed. The setup prepares the files of a messy repository, to upload in one go.
Setup: on GitHub, create a public repository homelab-practice-4, with no initial files. Then:
mkdir -p ~/git-practice/reorganize/upload && cd ~/git-practice/reorganize/upload
printf '# homelab\n' > README.md
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
printf 'print("backup")\n' > backup.py
printf -- '- pi-dns: Pi-hole\n' > notes.txt
Tasks:
- On the empty repository's Quick setup page, use the uploading an existing file link to upload the four files of
~/git-practice/reorganize/uploadin a single commit, with the messageImport homelab files. -
Using only the web editor, reach this layout, with one commit per change and meaningful messages:
README.md inventory.csv services.md (was notes.txt) scripts/backup.py (was backup.py) docs/.gitkeep (new, empty) -
Clone the repository into
~/git-practice/reorganize, and verify the layout and the history.
Expected result and verification:
- The first commit contains the four files:
git show --staton it lists 4 files. git log --onelineshows 4 commits: the import, the rename ofnotes.txt, the move ofbackup.py, and the creation ofdocs/.gitkeep.git show --staton the rename and move commits showsnotes.txt => services.mdandbackup.py => scripts/backup.pywith0changed lines.
Solution
- Rename: open
notes.txt, pencil icon, change the name toservices.md, commitRename notes.txt to services.md. - Move: open
backup.py, pencil icon, place the cursor at the start of the name field and typescripts/, commitMove backup script to scripts/. - Empty directory: Add file > Create new file, name
docs/.gitkeep, no content, commitAdd docs directory.
cd ~/git-practice/reorganize
git clone https://github.com/<username>/homelab-practice-4.git
cd homelab-practice-4
find . -path ./.git -prune -o -type f -print | sort
git log --oneline
git show --stat HEAD~2 # notes.txt => services.md
git show --stat HEAD~1 # backup.py => scripts/backup.py
HEAD~2andHEAD~1assume you made the changes in the order above; otherwise, use the hashes shown bygit log --oneline.- A move or rename with no content change is detected as a rename (
=>), sogit log --follow scripts/backup.pystill finds the import commit. - The upload was the only way to put four files in one commit without Git.
Clean up when you are done: rm -rf ~/git-practice/reorganize, and delete homelab-practice-4 on GitHub.