Files: .gitmodules and .lsm.lock

LazySubmodules keeps its data in two git-config files that are committed to the superproject: the tracking configuration and the lock file.

File

Content

Written by

.gitmodules

Git’s submodule configuration, plus lsm-mode and lsm-ref

add, set, update

.lsm.lock

The resolved ref and commit of each managed submodule

add, update

Both files live at the top level of the superproject. LazySubmodules reads and writes them only with git config -f <file>, so Git does the parsing and the quoting, and unknown keys stay in place.

Common rules

  • Sections. Each submodule has a section [submodule "<name>"]. The name is the submodule name; it may contain dots and spaces, and it is compared exactly. Section and key names are compared without regard to case, as Git does.

  • Duplicate keys. When a key occurs more than once, the last value wins.

  • Regular files. Both files must be regular files. A symbolic link in their place is refused (status 1), so that a cloned repository cannot redirect a write to a file outside of it:

    lazysubmodules: read .lsm.lock: config file $DEMO/firmware/.lsm.lock: not a regular file
    
  • Syntax errors. A file that Git cannot parse makes every command fail with the error of Git (status 5):

    lazysubmodules: read .gitmodules: git config -f .gitmodules --null --list: exit status 128: fatal: bad config line 76 in file .gitmodules
    
  • Which copy. status and verify read the copies in the working tree. update also reads the copies in the index and in HEAD, to find out what the superproject records.

.gitmodules

Git ignores keys it does not know, so the tracking configuration lives in the submodule sections of .gitmodules, under the lsm- prefix:

[submodule "kernel"]
	path = kernel
	url = https://git.example.org/linux.git
	lsm-mode = tag-pattern
	lsm-ref = v6.6.*

Keys

Key

Owner

Meaning

lsm-mode

LazySubmodules

Tracking mode: branch, tag, tag-pattern or commit

lsm-ref

LazySubmodules

Branch name, tag name, glob pattern, or commit SHA

branch

Git

Native key. LazySubmodules writes it for lsm-mode = branch and removes it for the other modes

path

Git

Path of the submodule; LazySubmodules reads it

url

Git

URL of the submodule; git submodule uses it to clone

update, ignore, shallow and others

Git

Left unchanged

lsm-mode and lsm-ref are a stable interface.

lsm-mode

  • Managed or not. A submodule without lsm-mode is unmanaged: status shows it as unmanaged, and no command modifies it until set puts it under management. A stray lsm-ref or lock entry of an unmanaged submodule is ignored.

  • Exact spelling. The value must be one of the four modes, spelled exactly. Any other value, including Tag, makes every command that reads the file fail with status 1:

    lazysubmodules: .gitmodules: submodule "kernel": lsm-mode: invalid tracking mode "semver"
    

lsm-ref

Mode

Value

Example

branch

Branch name on origin

main

tag

Tag name

v2.3.1

tag-pattern

Glob as accepted by git tag --list

v6.6.*

commit

Commit SHA in lowercase hexadecimal, with 7 to 40 digits (64 in SHA-256 repositories); add and set always store the full SHA

5da6927f70f5cb5408fe3a77c0aa33877a3062c5

  • Invalid values. A value that is empty, starts with -, contains white space or control characters, or is not a valid ref name makes the submodule missing-ref. update refuses it, and verify fails its lock-config check:

    lazysubmodules: kernel: refused: bad ref in .gitmodules: invalid tag pattern "": empty value
    
  • Existence. A value that is valid but does not resolve in the local refs also gives missing-ref.

The native branch key

Git uses submodule.<name>.branch for git submodule update --remote. LazySubmodules keeps it in step with the tracking mode:

  • Branch mode. add --branch, set --branch and update write branch = <lsm-ref>, so that git submodule update --remote follows the same branch without LazySubmodules.

  • Other modes. set and update remove the key. Without it, git submodule update --remote would use the default branch of the remote, which is not what a tag or commit pins; Work with plain Git shows the effect.

  • Repair. When the key does not follow the mode, update rewrites it and reports restored .gitmodules.

On the demo, set --branch main on the unmanaged submodule legacy writes all three keys:

$ git config -f .gitmodules --get-regexp '^submodule\.legacy\.'
submodule.legacy.path vendor/legacy
submodule.legacy.url https://git.example.invalid/legacy.git
submodule.legacy.branch main
submodule.legacy.lsm-mode branch
submodule.legacy.lsm-ref main

Other rules

  • Skipped entries. Entries that Git itself does not treat as a submodule are skipped: an entry without a path, one whose name is empty or has a .. component, and one whose path is not a clean relative path inside the working tree. As in Git, path and url values that start with - are ignored.

  • Native keys stay. update = none does not stop LazySubmodules from checking the submodule out, and ignore = all does not hide its real state from status.

  • Name of an added submodule. add names the submodule after its path, as git submodule add does.

Example

examples/.gitmodules is the file that the demo ends with, with comments. The URLs point to git.example.org and are only illustrations.

examples/.gitmodules
# The highest stable tag matching the glob; pre-releases such as v6.6.11-rc1
# count only with --include-prerelease.
[submodule "kernel"]
	path = kernel
	url = https://git.example.org/linux.git
	lsm-mode = tag-pattern
	lsm-ref = v6.6.*

# The tip of origin/main. The native branch key, which lazysubmodules writes
# for branch mode, keeps "git submodule update --remote" on the same branch.
[submodule "u-boot"]
	path = bootloader/u-boot
	url = https://git.example.org/u-boot.git
	branch = main
	lsm-mode = branch
	lsm-ref = main

# One tag. The name contains a dot and differs from the path.
[submodule "fpga.core"]
	path = ip/fpga-core
	url = https://git.example.org/fpga-core.git
	lsm-mode = tag
	lsm-ref = v2.3.1

# One commit, always written as the full SHA.
[submodule "crypto lib"]
	path = libs/crypto lib
	url = https://git.example.org/crypto-lib.git
	lsm-mode = commit
	lsm-ref = 5da6927f70f5cb5408fe3a77c0aa33877a3062c5

# Unmanaged (no lsm keys) until "lazysubmodules set legacy --branch main".
[submodule "legacy"]
	path = vendor/legacy
	url = https://git.example.org/legacy.git
	branch = main
	lsm-mode = branch
	lsm-ref = main

# A branch other than main; the submodule has a submodule of its own, which
# LazySubmodules does not manage.
[submodule "tools"]
	path = tools/nested
	url = https://git.example.org/tools.git
	branch = develop
	lsm-mode = branch
	lsm-ref = develop

[submodule "theme"]
	path = docs/theme
	url = https://git.example.org/theme.git
	lsm-mode = tag
	lsm-ref = v1.0.0

[submodule "fresh"]
	path = third_party/fresh
	url = https://git.example.org/fresh.git
	lsm-mode = tag-pattern
	lsm-ref = v1.*

[submodule "app"]
	path = apps/app
	url = https://git.example.org/app.git
	lsm-mode = tag
	lsm-ref = v1.2.0

# Every tag. The lock file records which one was selected.
[submodule "sdk"]
	path = sdk
	url = https://git.example.org/sdk.git
	lsm-mode = tag-pattern
	lsm-ref = v*

[submodule "mirror-lib"]
	path = libs/mirror
	url = https://git.example.org/mirror-lib.git
	branch = stable
	lsm-mode = branch
	lsm-ref = stable

[submodule "broken"]
	path = libs/broken
	url = https://git.example.org/broken.git
	lsm-mode = tag
	lsm-ref = v1.0.0

# "lazysubmodules verify --signatures" checks the signature of this tag.
[submodule "signed"]
	path = libs/signed
	url = https://git.example.org/signed.git
	lsm-mode = tag
	lsm-ref = v1.0.0

# Native keys stay as they are. LazySubmodules checks the submodule out even
# with update = none, and its status looks at the real state whatever
# ignore says.
[submodule "quirky"]
	path = libs/quirky
	url = https://git.example.org/quirky.git
	update = none
	ignore = all
	shallow = true
	lsm-mode = tag
	lsm-ref = v1.0.0

.lsm.lock

The lock file records, for each managed submodule, the mode and ref that were resolved and the commit they resolved to:

[submodule "kernel"]
	mode = tag-pattern
	ref = v6.6.9
	commit = 8106f614767a75b6567271b1db80d6ade3acb7fb

Keys

Key

Meaning

mode

Tracking mode used for the resolution

ref

Resolved ref: the tag for tag and tag-pattern, the branch for branch, the full SHA for commit

commit

Full commit SHA in lowercase: 40 hexadecimal digits, or 64 in SHA-256 repositories

For a tag pattern, ref is the tag that the pattern selected, so the lock file shows which release is in use: v6.6.* in .gitmodules, v6.6.9 in .lsm.lock.

Rules

  • Writers. add and update write the entry of a submodule and stage the file together with the gitlink. set never touches the lock file.

  • Order. A new entry is appended, so entries appear in the order in which they were first written. A rewritten entry keeps its place.

  • Other keys. Keys and sections that LazySubmodules does not know are ignored and kept.

  • Ignore rules. The file is staged even when an ignore rule such as *.lock matches it. Commit it to the superproject.

  • Missing file or entry. A missing file is an empty lock. A managed submodule without an entry is behind, and verify fails its lock-entry check.

  • Validation. Every entry must have a known mode, a ref that is not empty, does not start with - and has no white space or control characters, and a full lowercase SHA. Otherwise every command that reads the file fails with status 1:

    lazysubmodules: .lsm.lock: invalid lock entry "kernel": commit: "8106f61" is not a full lowercase hexadecimal SHA
    
  • Length of the SHA. A SHA of the wrong length for the repository, such as 64 digits in a SHA-1 repository, fails the lock-commit check of verify.

Why a lock file

Tags can be moved: a maintainer can force-push a release tag to another commit. The lock file records the commit that the tag had when the submodule was updated. After fetch has moved the local tag, status shows drift and verify fails with status 4. Detect moved tags walks through this.

The lock file is also what status compares with to report behind, and what the generated commit message uses for the old ref.

Example

examples/.lsm.lock belongs to examples/.gitmodules. Its commits are the real commits of the demo repositories.

examples/.lsm.lock
[submodule "kernel"]
	mode = tag-pattern
	ref = v6.6.10
	commit = 08dcd0dc8f98ab3da3943153cad056f346d18ded
[submodule "u-boot"]
	mode = branch
	ref = main
	commit = 8e9fc7a2511a8a14496d8458128c622ec4615c6e
[submodule "fpga.core"]
	mode = tag
	ref = v2.3.1
	commit = 7c6069a8a36b168725442e3067e9dee415bf740b
[submodule "crypto lib"]
	mode = commit
	ref = 5da6927f70f5cb5408fe3a77c0aa33877a3062c5
	commit = 5da6927f70f5cb5408fe3a77c0aa33877a3062c5
[submodule "tools"]
	mode = branch
	ref = develop
	commit = 3e8e30783478eb8f3bc000bbed9847a034b0f17a
[submodule "theme"]
	mode = tag
	ref = v1.0.0
	commit = 05f49f3f8b5a18447c1e10f18f28fc7e221d8110
[submodule "fresh"]
	mode = tag-pattern
	ref = v1.1.0
	commit = f2de3e1ec47a9de1bd0316651efbfce990587c49
[submodule "app"]
	mode = tag
	ref = v1.2.0
	commit = cfa9d86f3eae90a7660a9ab94d1386902f26fe3d
[submodule "sdk"]
	mode = tag-pattern
	ref = v3.0.0
	commit = 599ca2ab457637cf8ed802b8581e3a383a191e8b
[submodule "mirror-lib"]
	mode = branch
	ref = stable
	commit = 71eb52c533ca5105af902ec2f6bab8f0c1d8ac52
[submodule "broken"]
	mode = tag
	ref = v1.0.0
	commit = 52c4bdbcb500e78dc6ae9826b4b478165afa96b9
[submodule "signed"]
	mode = tag
	ref = v1.0.0
	commit = 5d1133e0516f426db2702661540e7f92fa4b4c89
[submodule "quirky"]
	mode = tag
	ref = v1.0.0
	commit = 64eca61e3a67b19004201c98582e249fda26b9f6
[submodule "legacy"]
	mode = branch
	ref = main
	commit = 2f54e11fe5a9c2cf222f3d113338e7e53ddc3620

The entry of legacy comes last: the demo puts that submodule under management with set, and its first update runs after the other entries exist.

Removing a submodule from management

Remove both tracking keys and the lock entry, then commit both files:

git config -f .gitmodules --unset submodule.fresh.lsm-mode
git config -f .gitmodules --unset submodule.fresh.lsm-ref
git config -f .lsm.lock --remove-section submodule.fresh
git add .gitmodules .lsm.lock

status fresh then shows unmanaged, and verify leaves the submodule out.

See also