Using Git
Clone one branch or tag, w/o history, with submodules w/o their history (shallow)
git clone -b v1.2.3 --recurse-submodules --shallow-submodules --depth 1 [repo]
Update a tag / release (rewrite an existing tag)
Use these Variables
tag: the GitHub tag name (e.g.v1.2.3)btag: temporary branch name created from the tagrtag: full tag ref path (refs/tags/<tag>), used for remote deletion
Example: tag=v1.2.3
btag="b$tag"
rtag="refs/tags/$tag"
1) Create and switch to a temporary branch from the existing tag
git fetch origin --tags
git switch -c "$btag" "$tag"
2) Make changes, commit, and push the temporary branch
git commit -am "Fixes for $tag"
git push -u origin "$btag"
3) Delete old local tag pointing to the old commit, recreate the tag locally at the new HEAD
(You’re already on $btag, so the extra git switch "$btag" is redundant.)
git tag -d "$tag"
git tag "$tag"
4) Replace the remote tag pointing to the new commit
First delete the old remote tag, then push the new one. Force-update the remote tag explicitly because tags are immutable by convention and many servers reject non-fast-forward tag updates unless forced.
git push origin ":$rtag"
git push --force origin "$rtag"
5) Update the GitHub Release (and assets) if necessary
- If you uploaded release assets (binaries / custom .tar.gz / etc.): update the existing release by removing the old assets and uploading the new ones (or delete and recreate the release). Moving the tag does not replace uploaded assets.
- If only GitHub-generated source archives are used (“Source code (tar.gz/zip)”): you can usually leave the release as-is; it will continue to be “for $tag”, and the generated archives will follow the updated tag (may take a short while due to caching).
- If release notes/changelog should change: either edit the existing release notes, or delete and recreate the release for $tag.
6) Cleanup: delete the temporary branch (remote + local)
git push origin --delete "$btag"
git switch main
git branch -D "$btag"
Branches
Create new branch and switch to it:
git checkout -b branchname
Switch to an existing branch:
git checkout branchname
Merge changes from branch1 into branch2 (one of them can be main). Use rebase for a clean history.
Alternatively, you can use merge to keep the history of both branches.
git checkout branch2
git merge/rebase branch1
Push a branch to remote:
git push -u origin branchname
Delete a remote branch:
git push origin --delete branchname
List all remote branches:
git branch -r: lists all remote branchesgit branch -a: lists all branches (local and remote)git branch -vv: lists all branches with their last commitgit branch --merged: lists branches that have been merged into the current branchgit branch --no-merged: lists branches that have not been merged into the current branch
Take specific files from another branch (not a full merge)
When you only want one or a few files from another branch (or commit/tag), don’t merge the whole branch — pull just those paths into your current working tree.
⚠️ These commands overwrite the local copy of the listed paths. Any uncommitted changes to them are lost. Run
git statusfirst if unsure.
Preview the difference first
git diff main..otherbranch -- path/to/file # what would change in that file
git show otherbranch:path/to/file # print the file as it is on otherbranch
Copy the files over (replace local version)
# Modern (recommended). --source can be a branch, tag, or commit SHA.
git restore --source=otherbranch --staged --worktree -- path/to/file [more/files...]
# Classic equivalent (also stages the result):
git checkout otherbranch -- path/to/file [more/files...]
git checkout <branch> -- <path> updates both the working tree and the index (already staged).
git restore --source=<ref> -- <path> updates only the working tree by default — add --staged (as above)
to also stage it. Then commit normally:
git add path/to/file # only needed if the file is not already staged
git commit -m "Bring path/to/file from otherbranch"
Take only some changes within a file (interactive, hunk by hunk)
This is the closest thing to “merging” a single file: pick which hunks to bring in.
git restore -p --source=otherbranch -- path/to/file # or: git checkout -p otherbranch -- path/to/file
True 3-way merge of a single file
checkout/restore replace the file; they do not merge its content with yours. For an actual
line-by-line merge of one file between two versions, use git merge-file:
git show otherbranch:path/to/file > /tmp/theirs # their version
git show $(git merge-base HEAD otherbranch):path/to/file > /tmp/base # common ancestor
git merge-file path/to/file /tmp/base /tmp/theirs # merges into path/to/file, marks conflicts
Conflicts
1. Resolve Conflicts
- Check what’s conflicted:
git status
- Text conflicts (
UU, etc.): open file and fix<<<<<<< ======= >>>>>>>, then:
git add path
- Keep one side (choose ours or theirs):
git checkout --ours path # keep our version (rebase source)
git checkout --theirs path # keep their version (rebase target)
git add path
- You want the file removed (e.g.
DU/ delete-vs-modify conflict):
git rm -- path
# if the file is already gone locally:
git rm --cached -- path
git add -u
2. Finalize a rebase or a merge
git rebase --continue # repeat until it finishes
git push origin branch2
or
git commit -m "merge branch1 into branch2"
git push origin branch2
Unmerged status codes
DDboth deletedAUadded by usUDdeleted by themUAadded by themDUdeleted by us (often: decide keep vs delete; usegit rmto delete)AAboth addedUUboth modified
History
Show commit + diff for a specific file
git log -p -- path/to/file
Compact one-line history
git log --follow --date=short --pretty=format:"%h %ad %an %s" -- path/to/file
To remove all history from a repo:
git checkout --orphan new-main
git add -A
git commit -m 'new files'
git branch -D main
git branch -m main
git push -f origin main
git branch --set-upstream-to=origin/main main
Tokens (Personal Access Tokens)
A Personal Access Token (PAT) is a credential tied to your personal account, not to a repository. You always create it under:
Personal Settings > Developer Settings > Personal access tokens
Two key facts that are easy to get wrong:
- The token inherits the permissions of the account that creates it. To act on a repo (trigger its workflows, push, pull private images), that account must have the matching access to that repo.
- GitHub shows the token string only once, at creation. It cannot be read back later (not from your settings, not from a secret it was pasted into). If you lose it, regenerate.
You can regenerate an existing token (new value) or edit a classic token’s scopes in place (value
unchanged) — both under the same page. Always set a custom expiration date.
Which scope / permission do I need?
Pick the row for what the token must do, not for the repo it lives in:
| Goal | Classic scope | Fine-grained permission |
|---|---|---|
| Trigger another repo’s workflow (API) | repo / public_repo |
Actions → Read and write |
Add / edit .github/workflows/* files |
workflow |
Workflows → Read and write |
| Pull private container images | read:packages |
Packages → Read |
⚠️ Common mistake: the
workflowscope is only for committing workflow files. It does not let you trigger a run. To fire aworkflow_dispatch/repository_dispatchyou needrepo(public_repofor public repos) on a classic token, or Actions: Read and write on a fine-grained token.
Direct links
- Fine-grained: https://github.com/settings/personal-access-tokens/new
→ Owner
gemc, repo e.g.gemc/clas12-systems, permission Actions: Read and write - Classic: https://github.com/settings/tokens/new
→ Scope: ✅
repo(coverspublic_repo); addread:packagesfor private images
A token used by a local tool (not a workflow) should go in a file read by that tool, typically in the home
dir, for example .bob.
Secrets
A GitHub Actions secret is an encrypted variable stored in a repository’s settings. Workflows in that
repository read it at runtime via ${{ secrets.SECRET_NAME }}; the value is never printed in logs or
shown to anyone browsing the repo.
Keep the two pieces separate — they live in different places:
- The token (the
ghp_…/github_pat_…string) is created in your personal account (above). - The secret is a copy of that string, stored in the repository whose workflow will use it — i.e. the repo that does the triggering, not the repo being triggered.
How to create and store it
-
Generate the token in your personal account (Developer settings), choosing the scope/permission for the target repo you want the workflow to act on (see the table above). The account must have write access to that target repo.
-
Copy the token value shown on screen (visible only once).
-
Add the secret to the repository whose workflow uses it
- Go to that repo → Settings → Secrets and variables → Actions (e.g. https://github.com/gemc/pygemc/settings/secrets/actions)
- Click New repository secret
- Name: e.g.
GEMC_SRC_PAT(must match the name referenced in the workflow YAML) - Value: paste the token
- Click Add secret
Triggering a workflow in another repository
The built-in GITHUB_TOKEN cannot start workflows in a different repository, so cross-repo triggers
use a PAT stored as a secret. The pattern: repo A’s workflow calls the GitHub API to dispatch repo
B’s workflow.
You need three things:
- A PAT owned by an account with write access to repo B, scoped to trigger Actions
(classic
repo, or fine-grained Actions: Read and write on B). See the Tokens table above. - The secret holding that PAT, stored in repo A (the trigger source).
- A dispatchable trigger on repo B’s workflow — add
workflow_dispatch:(with optionalinputs:) to theon:block of the target workflow, otherwise the API call returns404/422.
The dispatch call (in repo A’s workflow)
- name: Trigger workflow_dispatch on repo B
run: |
curl --fail-with-body -X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.B_PAT }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/<owner>/<repoB>/actions/workflows/test.yml/dispatches \
-d '{"ref":"main","inputs":{"triggered_by":"repo A"}}'
The inputs keys must match the inputs: declared under workflow_dispatch: in repo B’s workflow.
Security: guard workflow_run-chained triggers
A workflow with on: workflow_run runs privileged (it can see secrets/PATs) and fires for fork-PR
events too, so gate the job — otherwise a fork PR could reach the privileged step and any code it checks
out via workflow_run.head_sha. This is also why CodeQL flags “checkout of untrusted code in a privileged
context”.
Which field to check depends on the position in the chain — this trips people up:
-
1st hop — listening to a workflow triggered directly by
push/pull_request(e.g. aDeploythat runson: workflow_runofTest):github.event.workflow_run.eventis the original event, so guard withevent == 'push'.if: >- ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' }} -
2nd hop — listening to a workflow that is itself
workflow_run-triggered (e.g. a job that runs afterDeploy, which already runson: workflow_run): hereworkflow_run.eventis alwaysworkflow_run, neverpush. Usingevent == 'push'makes the job never run; usingevent == 'workflow_run'always passes (no protection). Guard on the source repo instead — thehead_*fields carry the original source through the chain:if: >- ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository }}head_repository.full_name == github.repositoryis true for same-repo pushes and false for fork-PR chains, which is exactly the untrusted-code-checkout protection you want.
Examples in this project
| Trigger source (repo A) | Secret name (in A) | Target (repo B) | Token needs (on B) |
|---|---|---|---|
gemc/pygemc |
GEMC_SRC_PAT |
gemc/src |
trigger Actions on src |
gemc/src |
CLAS12_SYSTEMS_PAT |
gemc/clas12-systems |
trigger Actions on clas12-systems |