Most people with Linux experience have at some point installed .deb
files on Debian or the more famous variant of it, Ubuntu. Programmers who have been involved with packaging and shipping software know that the code that generates those .deb
packages is always in the debian/
subdirectory in a software project. However, anyone who has tried to do Debian packaging also knows that all the automation involved can be challenging to grasp, and building packages, modifying packaging files, and repeatedly rebuilding them can feel way more frustrating than iterating in regular software development. As Debian has been around for three decades already, there is a lot of online documentation available, but unfortunately, most of it is outdated, and reading about old tools might just add to the confusion.
Thus, let me introduce an explainer of what the structure in 2025 should look like on a well-maintained Debian package source, and what benefits it brings. First, I’ll run through the basics to ensure all readers have them fresh in their mind, and further down, I get into the increasingly complex workings of how Debian source packaging works and why they have a certain git repository structure.
Native vs non-native package and the role of Debian revisions of form 1.0-1
The first thing to decide when working on a Debian package is whether it should be a so-called native package or a non-native package. In Debian, a native package means that the Debian packaging is developed together with the software to be packaged. So if upstream project Foo releases the version 1.0, and simultaneously Debian packages, the packages would be native and have the version 1.0.
A non-native package, on the other hand, means that the Debian package was created separately from the upstream project. A big key difference between native and non-native is that the native package contents cannot diverge from the upstream release contents, as they are by definition the same single entity. A non-native package, however, can diverge from upstream, and it can have multiple releases of its own, with designated Debian revisions. So for the example Foo 1.0, the non-native Debian packages could be 1.0-1, 1.0-2 and so forth, and they can contain custom patches that modify the behavior of Foo to run better or at least more suitable for users, in the view of the author of the Debian packaging.
This distinction is also key to understanding why the Debian packaging repositories have additional complexity. The non-native packages need to have clear separation of the upstreams they package, which leads to the need to have separate branches and patches and git tags to signify what commit was the Debian revision, separately from the upstream release.
Native packages are simple: one git branch, one debian/
subdirectory
For a native package, the story is simple. If the project is developed on, say, branch main
, then the native Debian packaging is done on that branch in the subdirectory debian/
. To build the native .deb
packages of a specific release, just git checkout
the project sources from the release git tag, for example, v1.0
, and build with dpkg-buildpackage (or some other tool that wraps around it with additional build environment management).
gitGraph: checkout main commit id: "Foo project 1.0" tag: "v1.0" tag: "debian/1.0" commit id: "Foo commit a1" commit id: "Foo commit b2" commit id: "Foo project 1.1" tag: "v1.1" tag: "debian/1.1" commit id: "Foo commit c3" commit id: "Foo commit d4"
This is easy to grasp, as there are no additional abstractions layered on top of this. Native packages are, however, quite rare and limited in practice to software projects where the developers and the Debian packagers are the same set of people. If you are not the upstream of a project yourself, you cannot do native Debian packages.
Non-native packages: multiple branches, patches inside git
For a non-native package, things are more complex. First of all, the non-native packaging needs a branch of its own to live on. In 2025, modern Debian packages use the branch name debian/latest
for all commits that improve the packaging. When a release is made from debian/latest
, the commit will get a git tag of form debian/1.0-1
(note the Debian revision -1
).
Second, the non-native packaging branch commits will only ever modify files in the debian/
subdirectory. Maintaining clear separation between what is upstream code and what is a modification done in the Debian packaging is crucial both for security and supply chain auditability reasons (as explained in detail later), and also for ensuring that long-term maintenance is straightforward by avoiding downstream and upstream changes getting mingled and mixed up.
If the non-native Debian package needs to have some upstream code slightly changed, for example, to make the software build correctly on an architecture untested by upstream, a patch file would be added in the patch debian/patches/
, which dpkg-source
(which is one of the subcommands that dpkg-buildpackage automatically runs) then applies to the upstream code at build-time. However, on the Debian packaging branches there will never be any permanently git committed changes to upstream code, i.e., outside the debian/
directory.
From upstream release branch to upstream import branch to Debian packaging branch
In addition to the main
and debian/latest
branches already explained, there is a third branch called upstream/latest
which acts as the target branch for upstream imports. If the upstream source code contains files that are not acceptable in Debian (and usually listed in the debian/copyright
file under the Files-Excluded
` section), they would be purged on this branch before this import branch gets merged into the Debian branch.
gitGraph: checkout main commit id: "Foo project 1.0" tag: "v1.0" branch upstream/latest commit id: "New upstream version 1.0" tag: "upstream/1.0" branch debian/latest commit id: "Update upstream source from tag 'upstream/1.0'" commit id: "Create initial Debian packaging" commit id: "Update changelog for 1.0-1 release into unstable" tag: "debian/1.0-1" checkout main commit id: "Foo commit a1" commit id: "Foo commit b2" commit id: "Foo project 1.1" tag: "v1.1" checkout upstream/latest merge main id: "New upstream version 1.1" tag: "upstream/1.1" checkout debian/latest merge upstream/latest id: "Update upstream source from tag 'upstream/1.1'" commit id: "Update changelog and refresh patches after 1.1 import" commit id: "Debian commit 1a" commit id: "Debian commit 2b" commit id: "Update changelog for 1.1-1 release into unstable" tag: "debian/1.1-1" checkout main commit id: "Foo commit c3"
In the rare case that an upstream software project is not using git for version control, the upstream release branch (e.g., main
) would not exist at all, and the upstream/latest
would only contain synthetic commits made by the Debian packager by importing the upstream .tar.gz
source package releases or equivalent.
%%{init: { 'gitGraph': { 'mainBranchName': 'upstream/latest' } } }%% gitGraph: checkout upstream/latest commit id: "New upstream version 1.0" tag: "upstream/1.0" branch debian/latest commit id: "Update upstream source from tag 'upstream/1.0'" commit id: "Create initial Debian packaging" commit id: "Update changelog for 1.0-1 release into unstable" tag: "debian/1.0-1" checkout upstream/latest commit id: "New upstream version 1.1" tag: "upstream/1.1" checkout debian/latest merge upstream/latest id: "Update upstream source from tag 'upstream/1.1'" commit id: "Update changelog and refresh patches after 1.1 import" commit id: "Debian commit 1a" commit id: "Debian commit 2b" commit id: "Update changelog for 1.1-1 release into unstable" tag: "debian/1.1-1"
Some packages might import both from the upstream release git tag and the upstream source tarball release to ensure maximum supply chain auditability.
There are several steps involved, but the whole process is automated. The developer doing Debian packaging would normally just run gbp import-orig --uscan
and everything else would be automatic. In a correctly configured Debian packaging repository, the gbp tool knows what the upstream git remote address is, and what is the form of release git tags to look for. In most packages, the gbp tool uses uscan to get the upstream release tarball, and when available also check signatures. Configuring it all is a bit complex, because there are multiple files to edit (mainly debian/control
, debian/changelog
, debian/copyright
, debian/watch
, debian/gbp.conf
, debian/upstream/metadata
and upstream/signing-keys.asc
), but it is a one-off effort that won’t need re-doing unless upstream changes how new releases are done.
Verifying upstream release signatures with OpenPGP and pristine-tar
For a complete audit trail, three branches aren’t actually enough yet. There is also a fourth branch named pristine-tar
. This branch is never merged on any other branch. Its only purpose is to hold the xdelta data files, which combined with the git repository contents can be used to reconstruct the exact upstream release tarball (if the format is supported by pristine-tar).
Being able to reproduce the bit-by-bit exact upstream release tarball out of the git repository is important so that tarball signatures can be verified and source authenticity checked. This naturally works only for upstreams that publish signed release tarballs (typically releasing .asc
files along with their tarballs).
Upstreams might also sign their release git tags, but currently git-buildpackage does not support checking them automatically for authenticity (see Bug#839866).
Maintaining multiple generations of a package across several Debian and Ubuntu releases (or other downstreams)
Debian (and Ubuntu) releases are very conservative. Once a release is made, updates to the packages are not accepted unless there are serious bugs or security issues that absolutely need to be fixed. If maintenance releases are made, they would all branch off from the debian/latest
branch at the commit that was the last upload into the respective release. For example, if Foo 1.0-1 in Debian Bookworm needs to have an update, a new branch debian/bookworm
would be branched off tag debian/1.0-1
and released as Foo 1.0-1+deb12u1 and tagged in git as debian/1.0-1+deb12u1
.
gitGraph: checkout main commit id: "Foo project 1.0" tag: "v1.0" branch upstream/latest commit id: "New upstream version 1.0" tag: "upstream/1.0" branch debian/latest commit id: "Update upstream source from tag 'upstream/1.0'" commit id: "Create initial Debian packaging" commit id: "Update changelog for 1.0-1 release into unstable" tag: "debian/1.0-1" branch debian/bookworm commit id: "Backport fix 1a for Bookworm" commit id: "Update changelog for 1.0-1+deb12u1 release into unstable" tag: "debian/1.0-1+deb12u1" checkout main commit id: "Foo commit a1" commit id: "Foo commit b2" commit id: "Foo project 1.1" tag: "v1.1" checkout upstream/latest merge main id: "New upstream version 1.1" tag: "upstream/1.1" checkout debian/latest merge upstream/latest id: "Update upstream source from tag 'upstream/1.1'" commit id: "Update changelog and refresh patches after 1.1 import" commit id: "Update changelog for 1.1-1 release into unstable" tag: "debian/1.1-1" checkout main commit id: "Foo commit c3"
Note that these maintenance releases always release with semantically lower versions than the latest version on the debian/latest
branch. This ensures that any system with the latest maintenance branch version installed will on a full system upgrade pick the latest version from a newer Debian release.
Try it yourself: example repository galera-4-debian-demo
To fully grasp step-by-step what is happening, try doing a new upstream version import on a Debian packaging yourself using the example package repository prepared specifically for this exercise.
You don’t need to be a Debian (or Ubuntu) expert to test this. Any Linux user can easily start a Debian unstable (sid) Linux container by running podman run --interactive --network host --tty --rm --shm-size=1G -e DISPLAY=$DISPLAY --volume=$PWD:/tmp/run --workdir=/tmp/run debian:sid bash
and then inside the container install the required software, configure temporary dummy settings, and run the import. Passing the DISPLAY
will allow launching graphical programs from the container, and mounting the current host system path as a volume inside the container will ensure the created git repository will persist after exiting the container.
apt update && apt install --yes --no-install-recommends git-buildpackage pristine-tar git-gui gitk
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
export DEBEMAIL="you@example.com"
gbp clone --add-upstream-vcs https://salsa.debian.org/otto/galera-4-debian-demo.git
cd galera-4-debian-demo
gitk --all &
Note in the gitk
window what it tells about the git commits, branches, and tags. Then proceed to run the import command:
gbp import-orig --uscan --no-sign-tags
gbp:info: Launching uscan...
Newest version of galera-4 on remote site is 26.4.21, local version is 26.4.20
=> Newer package available from:
=> https://releases.galeracluster.com/galera-4/source/galera-4-26.4.21.tar.gz
gpgv: Signature made Tue Nov 26 20:07:41 2024 +00:00
gpgv: using RSA key 3D53839A70BC938B08CDD47F45460A518DA84635
gpgv: Good signature from "Codership Oy (Codership Signing Key) <info@galeracluster.com>"
Leaving ../galera-4_26.4.21.orig.tar.gz where it is.
gbp:info: Using uscan downloaded tarball ../galera-4_26.4.21.orig.tar.gz
gbp:info: Importing '../galera-4_26.4.21.orig.tar.gz' to branch 'upstream/latest'...
gbp:info: Source package is galera-4
gbp:info: Upstream version is 26.4.21
gbp:info: Replacing upstream source on 'debian/latest'
gbp:info: Running Postimport hook
gbp:info: Successfully imported version 26.4.21 of ../galera-4_26.4.21.orig.tar.gz
After successfully running the import, switch focus to the gitk
window and press F5
to reload the view and see all branches and tags how they look now after the import.
Managing patches with gbp pq
As explained above, Debian has good reasons to carry patch files in the debian/patches
subdirectory separately from upstream code, and only apply it at build time. These files are cumbersome to edit manually, and they really shouldn’t be.Instead, the far superior way is to use gbp pq
, which converts the patches to a temporary branch, where each patch represents a single commit. Package maintainers can then use regular git commands to rebase, amend and cherry-pick those commits and test that builds work.
To activate this branch, simply run gbp pq switch --force
. The --force
ensures that any existing patches-applied branch will automatically be deleted and overwritten, in case such a branch was left around from a previous patch gbp pq
session. When all the commits on the temporary branch are final, convert them back to debian/patches
contents on the packaging branch by running gbp pq export --drop --commit
.
Managing the debian/changelog
with gbp dch
The git log is of course all that developers need for working with git. However, when the package is eventually built and shipped, the end users won’t see any git repositories but only the end result - the package and its files. Therefore Debian packagers maintain an extra debian/changelog
file that summarizes to end users what changed in each Debian release.
Maintaining the changelog is greatly simplified with the two following commands. Immediately after a new upstream import one would typically run gbp dch --distribution=UNRELEASED -- debian/
to put the changelog in an UNRELEASED state showing to other packagers that the next upload is still being prepared. The -- debian/
at the end is important to tell git-buildpackage that only changes in the Debian packaging should be considered and all upstream changes ignored.
Later, right before the upload, one would typically run gbp dch --release --commit
to finalize the changelog based off the git commit log entries.
Once an upload has been made, gbp tag
should be used to automatically add the correct git tags to the repository.
Benefit: complete record of software provenance for the sake of copyright, security and long-term maintainability
The multiple layers of versioning may seem complicated at first, but everything exists for a purpose. This git structure allows for very good software provenance, where the origin of any line of code can be tracked across multiple branches and tags. Learning to manage it all may require some initial effort, but once the most common git-buildpackage commands are familiar, using it is quite fast.
When working with big and complex packages a packager typically ends up having to debug a lot of build failures and do detective work to understand where a change came from and why it was made in order to fix the failures correctly. In modern Debian packaging repositories it is incredibly empowering to be able to run git blame
on any file and see all the upstream and Debian changes, or to simply compare across tags or branches how files are different with commands such as git difftool --dir-diff upstream/latest -- debian/
(with meld in screenshot below) to compare how the debian/
contents on the current branch differ from another branch.
Hosting the upstream release branch and the Debian packaging branch in the same repository provides near perfect software provenance, which goes a long way to manage copyright and security issues which are of high importance in Debian.