From 78220a44bb19a0f8e556d81b6851888ab47b2b81 Mon Sep 17 00:00:00 2001
From: dmerejkowsky Usage&
--correct-branch
/-c
flag is set, then the branch is changed to
+and the --no-correct-branch
flag is NOT set, then the branch is changed to
the configured one and then the repository is updated. Otherwise that repository
will not be not updated.Sync algorithm
tsrc
is a command-line tool that helps you manage groups of git repositories.
It works by listing the repositories in a file called manifest.yml
that looks like this:
repos:\n - dest: foo\n url: git@example.com:foo.git\n\n - dest: bar\n url: git@example.com:bar.git\n
You can then use:
tsrc init <manifest url>
to create a workspace containing the foo
and bar
repository
tsrc sync
to synchronize all repos in the workspace.
... and many more commands. Run tsrc help
to list them, or read the command line reference
Interested in using tsrc
in your own organization?
Proceed to the getting started tutorial!
"},{"location":"#guides","title":"Guides","text":"Once you've learn how to setup tsrc for your organization, feel free to read the following guides - tsrc supports a variety of use cases beyond just listing git repositories to be cloned or synchronized and are described here:
tsrc
now makes sure repos included via groups are processed before the other repos. See #356 for details. Thanks to @raabf for the bug report and code review!tsrc init
: fix order of operations - clone the local manifest before writing the workspace configuration. Fixes #344, where users could not run init
a second time if the previous call failed. Bug report by @cgestes.tsrc init
: do not assume the default branch of the manifest is master
. Note that master
is still hard-coded in a few places. See #347 for details.-j 1
, do not sort repositories by lexical order of destination, but preserve the order in which they were specified in the manifest. Suggested by @raabf.python -m tsrc
in addition to just tsrc
ignore_submodules
repository option - Patch by Thomas Hiscock.-j
is not used, try getting the default jobs
value from the TSRC_PARALLEL_JOBS
environment variable. Patch by Marcin Jaworski.attr
tsrc sync
now uses parallel jobs by default. Use -j1
to force sequential processing. Patch by @gdubickiAll of tsrc
commands can now be run in parallel. Try for instance tsrc sync -j auto
.
tsrc foreach
now sets a bunch of environment variables. This allows developers to add new behaviors to tsrc without having to change its source code. See the relevant guide for more information.
Augment documentation with more use cases and examples (still a work in progress)
Remove tsrc version
- Use tsrc --version
instead.
The 'parallel' feature caused the output of some commands like foreach
or log
to change slightly. Hopefully tsrc
output is now more consistent.
Fix crash when running tsrc
without any arguments
Fix crash when trying to clone repositories in some rare corner cases (like the destination existing but not being a directory)
tsrc
imports consistentrepr
on tsrc
Errors.tsrc apply-manifest
now performs file system operationblack
, mypy
, isort
...)copier
to simplify maintenance of tools configurationtsrc
calls git clone
with --recurse-submodules
when adding missing repositoriestsrc
calls git submodule update --init --recursive
when updating repositoriesmain
.Project has been moved from TankerHQ
organization to dmerejkowsky
. New urls are:
Add CI jobs to check this project also works with Python 3.9
Path Pie
dependencytsrc sync
and tsrc init
can now create symlinks as specified in the manifest file:
repos:\n\n - url: git@gitlab.local:proj1/app\n dest: app\n symlink:\n - source: app/some_file\n target: ../some_file\n
In this case, a symlink will be created from <workspace>/app/some_file
to <workspace>/some_file
. (both source
and target
keys are relative to the repository's destination).
--group
option and the --all-cloned
options--groups-from-config
options since this is now the default behaviorlint.sh
It was discovered that the manifest syntax was confusing for newcomers, so we decided to update it.
In particular, the src
key meant both a relative path in the workspace when used in the repo
config, and a relative path in the a repository when using in the repo.copy
config.
Starting with this release, repo.src
becomes repo.dest
and repo.copy.src
becomes repo.copy.file
.
# Before (tsrc < 2.1.0)\nrepos:\n url: \"https://acme.corp/foo\"\n src: foo\n copy:\n src: some-file\n dest: some-file\n
# After (tsrc >= 2.1.0)\nrepos:\n url: \"https://acme.corp/foo\"\n dest : foo\n copy:\n file: some-file\n dest: some-file\n
This should make it clearer what tsrc
does because:
dest
now always refers to a relative path in the workspace (both in repo
and copy
).repo.copy.file
it's obvious that tsrc
only supports copying files, not directories.Drop support for Python 3.5
"},{"location":"changelog/#new_features","title":"New features","text":"tsrc init
learned a -r, --remote
option that pins the remote with the given name as the only remote to be used for cloning and syncing. tsrc
expects this remote to be present in the manifest for all repositories. This is useful if you use the same workspace in different physical locations, and one of the remotes is behind a VPN for instance. Patch by @tronje.copy
statements in repos
libgit2
instead of running git commands in the tests helpers).safety
path
library.Remove the tsrc push
command and all review automation features. Please use hub, lab, or repo instead. See #207 for the discussion leading to this removal.
Implement small improvements on tsrc
output messages.
tsrc apply-manifest
, to apply changes in a manifest file locally, without having to make a commit and push to a server first.python_requires
value in project metadatatsrc init
with a list of groups.Starting the new year with a stable release, at last!
"},{"location":"changelog/#revamp_group_ux","title":"Revamp group UX","text":"The changes below in the configuration file and command line syntax allow for better UX regarding groups. See the corresponding milestone for the full list.
"},{"location":"changelog/#new_configuration_file","title":"New configuration file","text":"Previously, tsrc
stored its permanent configuration in .tsrc/manifest.yml
and the file was not supposed to be edited by hand. Instead, users could use tsrc init
to modify it, for instance with the --branch
argument.
Starting with this release, the command tsrc init
can only be run once per workspace, and you must edit the .tsrc/config.yml
file instead.
tsrc init
: remove --file
option.tsrc foreach
: instead of repeating the --group
option, you can use --groups
with a list of groups:# before\ntsrc init --group foo --group bar\n\n# after\ntsrc init --groups foo bar\n
tsrc init
learned a --clone-all-repos
option to clone all repositories from the manifest, regardless of the groups. Fix #181
Remove --file
option from tsrc init
.
tsrc foreach
learned a --groups-from-config
option to use the groups configured in the workspace. Fix #178, #179.
tsrc push
learned a -o, --origin
option to specify a remote name different from \"origin\". Fix #170
tsrc push --approvers
on GitLab Community Edition. (#165)master
branch.tsrc status
: add information when local branch does not match manifest configuration. (#190). Feature suggested by @janjachnicktsrc push --reviewer
on GitLab Community Edition)tsrc foreach
fails to start the process. Suggested by @dlewis-ald in #163tsrc
configuration file. Fix #158tsrc status
on a workspace with missing repositories (#160) - reported by @blastrocktsrc sync --force
. Currently all it does is running git fetch --force
on all repositories. Use with caution. See #152 for details.tsrc sync
when the repo
configuration in the manifest contained neither an URL nor a remote. tsrc
now aborts as soon as the misconfiguration of the manifest is detected (Reported by @jongep86)--file
option to tsrc init
so that manifest can be read from a custom path in the file systemxdg
to pyxdg
codecov.io
to measure coveragepython-cli-ui
.TankerHQ
Fix crash when using tsrc push
on a GitHub repository for the first time.
Fix weird output when configuring remotes.
"},{"location":"changelog/#v060_2018-10-09","title":"v0.6.0 (2018-10-09)","text":""},{"location":"changelog/#add_support_for_multiple_remotes","title":"Add support for multiple remotes","text":"# still valid (implicit 'origin' remote)\nsrc: foo\nurl: git@github.com/foo\n\n# also valid (two explicit remotes)\nsrc: foo\nremotes:\n - { name: origin, url: git@github.com:john/foo }\n - { name: upstream, url: git@github.com:foo/foo}\n\n# not valid (ambiguous)\nsrc: foo\nurl: git@github.com:john/foo\nremotes:\n - { name: upstream, url: git@github.com:foo/foo }\n
Thanks @tst2005 and @cgestes for their help with the configuration format.
"},{"location":"changelog/#tsrc_foreach","title":"tsrc foreach","text":"tsrc foreach
: add a --group
option to select the repositories to run the command on. Fix #40-r,--approvers
option in tsrc push
(GitLab Enterprise Edition only).tsrc push
was no longer able to create a merge request on GitLab if --target
was not set.tsrc push
: new features and bug fixesSee below for the details.
"},{"location":"changelog/#preliminary_github_support","title":"Preliminary GitHub support","text":"tsrc
from a repository which has a URL starting with git@github.com
.tsrc
will prompt you once for your login and password and then store an API token.
Afterwards, you'll be able to use tsrc
push to:
-a/--assignee
option)--reviewers
option)--merge
option)This change has no impact if you were already using GitLab
.
tsrc push
: new features and bug fixes","text":"--close
option.-m/--message
option is gone, use --title
instead. There's a concept of \"description\" or \"message\" for pull requests and merge requests, but the value of the option was only used to update the title, so it had to be renamed.tsrc push <local>:<remote>
to explicitly specify local and remote branch names.tsrc push
(see issue #80). Patch by @maximerety.Breaking change: Instead of using fixed_ref
in the manifest, you should now use tag
or sha1
:
old:
repos:\n - src: git@example.com/foo\n fixed_ref: 42a70\n
new:
repos:\n - src: git@example.com/foo\n tag: v0.1\n
See the dedicated section about manifest format and the #57 pull request discussion for the details.
This allow us to implement different behaviors depending on whether or not the fixed ref is a tag or just a sha1.
"},{"location":"changelog/#support_for_shallow_clones","title":"Support for shallow clones","text":"To save time and space, you can use tsrc init --shallow
to only have shallow clones in your workspace.
Note that due to limitations in git
itself, the shallow
option cannot be used with a fixed SHA1. If you need this, prefer using a tag
instead.
Organization TankerApp
was renamed to TankerHQ
. New urls are:
We now use pipenv for dependency handling.
tsrc status
to handle tags. Patch by @arnaudgelas.tsrc version
.tsrc status
output. Now also shows number of commits ahead and behind, and display a short SHA-1 when not on any branch. Initial patch by @arnaudgelas.Breaking change: Add support for groups (#30). Reported by @arnaudgelas.
See the dedicated section about manifest format for details.
Upgrading from v0.2.4:
To upgrade from an older version of tsrc
, you should re-run tsrc init
with the correct url:
# Check manifest URL:\n$ cd <workspace>/.tsrc/manifest\n$ git remote get-url origin\n# Note the url, for instance ssh://git@example.com:manifest.git\n$ cd <workspace>\n$ tsrc init <manifest-url>\n
This is required to create the <workspace>/.tsrc/manifest.yml
file which is later used by tsrc sync
and other commands.
tsrc push --assignee
: fix when there are more than 50 GitLab users (#25). Reported by @arnaudgelasSplit user interface functionality into its own project: python-cli-ui.
Add --quiet
and --color
global options.
Bug fix release.
tsrc init
: Fix crash when a repository is empty (#17). Reported by @nicolasbrechettsrc push
: Fix rude message when credentials are missing (#20). Reported by @cgestesPackaging fixes.
"},{"location":"changelog/#v020_2017-08-09","title":"v0.2.0 (2017-08-09)","text":"New syntax is:
repos:\n - src: foo\n url: git@gitlab.com:proj/foo\n branch: next\n\n - src: bar\n url: git@gitlab.com:proj/bar\n branch: master\n fixed_ref: v0.1\n
Note that branch
is still required.
dest
part of the copy
section if src
and dest
are equal:copy:\n - src:foo\n\n# same thing as\ncopy:\n - src: foo\n dest: foo\n
"},{"location":"changelog/#v014_2017-08-04","title":"v0.1.4 (2017-08-04)","text":"Support for Python 3.3, 3.4, 3.5 and 3.6
"},{"location":"changelog/#v011_2017-08-02","title":"v0.1.1 (2017-08-02)","text":"First public release
"},{"location":"code-manifesto/","title":"Code Manifesto","text":""},{"location":"code-manifesto/#basics","title":"Basics","text":"We use black
to enforce a coding style matching PEP8.
In addition, every text file must be pushed using UNIX line endings. (On Windows, you are advised to set core.autocrlf
to true
in your git config file.)
# Yes\ndef bar():\n \"\"\" bar stuff \"\"\"\n a = \"foo\"\n\n\n# No\ndef bar():\n ''' bar stuff '''\n a = 'foo'\n\n# Exception\nmy_str = 'It contains some \"quotes\" inside'\n
Use the fact that empty data structures are falsy:
# Yes\nif not errors:\n ...\n# No\nif len(errors) == 0:\n ...\n
Avoid using double negatives:
# Yes\ndef make_coffee(sugar=False):\n if sugar:\n print(\"with sugar\")\n\n# No\ndef make_coffee(without_sugar=True):\n if not without_sugar:\n print(\"with sugar\")\n
Prefer using \"f-strings\" if possible, +
may also work in some contexts.
# Yes\nmessage = f\"Welcome {name}!\"\n\n# No\nmessage = \"Welcome, {}!\".format(name)\nmessage = \"Welcome, %s!\" % name\nmessage = \"Welcome, \" + name + \"!\"\n\n# Okayish\nwith_ext = name + \".txt\"\n
textwrap.dedent()
to build nice-looking multi-lines strings:# Yes\ndef foo():\n long_message = textwrap.dedent(\"\"\"\\\n first line\n second line\n third line\"\"\")\n\n# No\ndef foo():\n long_message = \"\"\"\\\nfirst line\nsecond line\nthird line\n\"\"\"\n
# Yes\nok, mess = run_command()\n\nfor test_result in test_results:\n outcome, message = res\n\n# No\nfoo, bar = False, \"\"\n\nclass Foo:\n self.bar, self.baz = None, True\n
# Yes\nif foo:\n a = \"ok\"\nelse:\n a = \"nope\"\n\n\n# No:\na = \"ok\" if foo else \"nope\"\n
if ... in ...
when you can:# Yes\nif value in (\"option1\", \"option2\"):\n ...\n\n# No\nif value == \"option1\" or value == \"option2\"\n ...\n
"},{"location":"code-manifesto/#doc_strings_and_comments_in_production_code","title":"Doc strings and comments in production code","text":"First off, bad comments are worse that no comments.
Also note that you should use comments to explain why, never what. If the what is no clear, it means the behavior of the function or method cannot be easily understood by reading implementation, and so you should fix the implementation instead.
In conclusion, use comments and doc strings sparingly: that way, they will not rot and they will stay useful.
Note: this does not apply for tests (see below).
"},{"location":"code-manifesto/#collections","title":"Collections","text":"# Yes\nlist_1.extend(list_2)\n\n# No\nlist_1 += list_2\n
* Only use list()
and dict()
to convert a value to a list or dict. Prefer literals when possible # Yes\nmy_list = []\nmy_dict = {}\n\n# Also yes:\nmy_list = list(yield_stuff())\n\n# No\nmy_list = list()\nmy_dict = dict()\n
# Yes\nmy_copy = list(my_list)\n\n# Also yes:\nmy_copy = copy.copy(my_list)\n\n# No\nmy_copy = my_list[:]\n
# Yes\nmy_list = [foo(x) for x in other_list]\n\n# No\nmy_list = list()\nfor x in other_list:\n x.append(foo(x))\n\n# Also no\nmy_list = map(foo, other_list)\n\n# Yes\neven_nums = [x for x in nums if is_even(x)]\n\n# No\neven_nums = filter(is_even, nums)\n
# Yes\nmax(len(x) for x in my_iterable)\n\n# No\nmax([len(x) for x in my_iterable])\n
for result in results:\n # do something with result\n
"},{"location":"code-manifesto/#functions","title":"Functions","text":"Prefer using keyword-only parameters when possible:
# Yes\n# If the parameter needs a default value:\ndef foo(bar, *, spam=True):\n ...\n\n# If it does not:\ndef foo(bar, *, spam):\n ...\n\n\n# No\ndef foo(bar, spam=True):\n ...\n
If you use the last form, Python will let you use foo(42, False)
, and set spam
to False. This can cause problems if someone ever changes the foo
function and adds a new optional argument before spam
:
def foo(bar, eggs=False, spam=True):\n ...\n
After such a change, the line foo(42, False)
which used to call foo
with spam=False
now calls foo
with bar=False
and spam=True
, leading to all kinds of interesting bugs. Exception to this rule: when the keyword is obvious and will not change:
def get(value, default=None):\n ...\n
"},{"location":"code-manifesto/#imports","title":"Imports","text":"For any foo.py
file, import foo
must never fail, unless there is a necessary module that could not be found. Do not catch ImportError
unless it is necessary, for instance to deal with optional dependencies.
import required_module\n\nHAS_NICE_FEATURE = True\ntry:\n import nice_lib\nexcept ImportError:\n HAS_NICE_FEATURE = False\n\n#...\n\nif HAS_NICE_FEATURE:\n #....\n
Importing Python files should never cause side effects. It's OK to initialize global variables, but you should never call functions outside a if __name__ == main() block
.
Prefer using fully-qualified imports and names:
# Yes\nimport foo.bar\nmy_bar = foo.bar.Bar()\n\n# No\nfrom foo import bar\nmy_bar = bar.Bar()\n
Note
We allow a few exceptions like from pathlib import Path
or importing classes directory in tests. Use your best judgment.
abc.ABCMeta
instead of raising NotImplementedError
. This way you get the error when the class is instantiated instead of when the method is called.# Yes\nclass AbstractFoo(metaclass=abc.ABCMeta):\n @abc.abstractmethod\n def foo(self):\n pass\n\n\n# No\nclass AbstractFoo:\n def foo(self):\n raise NotImplementedError()\n
get_
methods.# Yes\nclass Person:\n def __init__(self, first_name, last_name):\n self.first_name = first_name\n self.last_name = last_name\n\n @property\n def full_name(self):\n return f\"{self.first_name} {self.last_name}\"\n\n\n# No:\nclass Foo:\n def __init__(self, first_name, last_name):\n self.first_name = first_name\n self.last_name = last_name\n self.full_name = f\"{self.first_name} {self.last_name}\"\n
For instance, here:
full_name
is read-onlyfirst_name
changes after the object is initialized.Note that get_
methods are OK if they do more than simple computations (expensive in time or size, throwing exceptions ...)
path.py
library and suffix the variable by _path
. Avoid using os.path
or shutil
methods when path.py
is better.# Yes\nwork_path = Path(\"foo/work\")\nwork_path.mkdir_p()\nfoo_path = work_path / \"foo.txt\"\nfoo_path.write_text(\"this is bar\")\n\n# No\nwork_path = os.path.join(foo, \"work\")\nos.path.mkdir(work_path, exist_ok=True)\nfoo_path = os.path.join(work_path, \"foo.txt\")\nwith open(foo_path, \"w\") as fileobj:\n fileobj.write(\"this is foo\")\n
"},{"location":"code-manifesto/#error_handling","title":"Error handling","text":"tsrc
should derive from tsrc.Error
.Do not use print
, use python-cli-ui functions instead. This makes it easier to distinguish between real messages and the throw-away print
statements you add for debugging.
Also, using \"high-level\" methods such as ui.info_1()
or ui.warning()
will make it easier to have a consistent user interface.
If you think the test implementation is complex, add a human-readable description of the test scenario in the doc string.
For instance:
def test_sync_with_errors(...):\n \"\"\"\" Scenario:\n * Create a manifest with two repos (foo and bar)\n * Initialize a workspace from this manifest\n * Push a new file to the foo repo\n * Create a merge conflict in the foo repo\n * Run `tsrc sync`\n * Check that the command fails and produces the proper error message\n \"\"\"\n
"},{"location":"code-manifesto/#assertions_with_lists","title":"Assertions with lists","text":"# Yes\nactual_list = function_that_returns_list()\n(first, second) = actual_list\nassert first == something\nassert second == something_else\n\n# NO\nactual_list = function_that_returns_list()\nassert len(actual_list) == 2\nfirst = actual_list[0]\nsecond = actual_list[1]\nassert first == something\nassert second == something_else\n
"},{"location":"code-manifesto/#assertion_order","title":"Assertion order","text":"When writing assertions, use the form assert <actual> == <expected>
:
# Yes\ndef test_foo():\n assert foo(42) == True\n\ndef test_big_stuff():\n actual_result = ...\n expected_result = ...\n\n assert actual_result == expected_result\n\n\n# No\ndef test_foo():\n assert True == foo(42)\n\n\ndef test_big_stuff():\n actual_result = ...\n expected_result = ...\n\n assert expected_result == actual_result\n
Rationale:
assert(expected, actual)
convention comes from JUnit but we are not writing Java code, and besides, the assert(actual, expected)
convention also exists in other tools.pytest
does not really care, but we prefer being consistent in all tests.The t
stands for tool
and src
for sources
.
If you speak French, you can also remember the name as \"tes sources\".
"},{"location":"faq/#why_not_repo","title":"Why not repo?","text":"We used repo for a while, but found that tsrc had both a better command line API and a nicer output.
On a less subjective level:
Good support for Windows (no need for Cygwin or anything like that)
Also, tsrc tries hard to never do any destructive operation or unexpected actions.
For instance, tsrc
never puts you in a \"detached HEAD\" state, nor does automatic rebase. It also never touches dirty repositories.
This is achieved by using mostly 'porcelain' commands from git, instead of relying on plumbings internals.
Also (and this matters a lot if you think about contribution):
black
mypy
Note that there are a few features present in repo
that are missing from tsrc
(but may be implemented in the future). Feel free to open a feature request if needed!
All this projects are fine but did not match our needs:
In any case, now that the whole team is using tsrc
all the time, it's likely we'll keep using tsrc
in the future.
It's all about workflow.
With git-submodule
, you have a 'parent' repository and you freeze the state of the 'children' repositories to a specific commit.
It's useful when you want to re-build a library you've forked when you build your main project, or when you have a library or build tools you want to factorize across repositories: this means that each 'parent' repository can have its children on any commit they want.
With tsrc
, all repositories are equal, and what you do instead is to make sure all the branches (or tags) are consistent across repositories.
For instance, if you have foo
and bar
, you are going to make sure the 'master' branch of foo
is always compatible to the 'master' branch of bar
.
Or if you want to go back to the state of the '0.42' release, you will run: tsrc foreach -- git reset --hard v0.42
.
Note that since tsrc 0.2
you can also freeze the commits of some of the repositories.
Last but not least, if you really need to use fixed references, you may do so by adding a sha1
or tag
line to the manifest. See the relevant guide for more details.
First off, we do use pygit2
, but only for tests.
Second, the pygit2
package depends on a 3rd party C library (libgit2
) - and that can cause problems in certain cases. If we can, we prefer using pure-Python libraries for the production code.
Finally, we prefer calling git \"porcelain\" commands, both for readability of the source code and ease of debugging (see below).
"},{"location":"faq/#why_do_you_hide_which_git_commands_are_run","title":"Why do you hide which git commands are run?","text":"It's mainly a matter of not cluttering the output. We take care of keeping the output of tsrc
both concise, readable and informative.
That being said:
--verbose
flag, like so: tsrc --verbose sync
It's nice to read and write, and we use the excellent ruamel.yaml which even has round-trip support.
Also, being Python fans, we don't mind that white space is part of the syntax.
"},{"location":"faq/#why_do_i_have_to_create_a_separate_git_repo_with_just_one_file_in_it","title":"Why do I have to create a separate git repo with just one file in it?","text":"See #235 for why you can't have multiple manifest files in the same repository.
Also, note that you can put other files in the repo - for instance, add a CI script that verifies the yaml syntax and checks that all the repos in the manifest can be cloned.
"},{"location":"getting-started/","title":"Getting started","text":""},{"location":"getting-started/#requirements","title":"Requirements","text":"Python 3.7 or later
"},{"location":"getting-started/#installing_tsrc","title":"Installing tsrc","text":"The recommended way to install tsrc
is to use pipx. This is because pipx
automatically creates isolated environment for each app, so you won't get into dependencies versions conflicts and won't have to deal with manual virtualenvs management.
pip
will also work, but it will not give you these benefits.
Recommended:
pipx install tsrc\n
Acceptable, if you know what you are doing:
pip install tsrc\n
"},{"location":"getting-started/#checking_tsrc_installation","title":"Checking tsrc installation","text":"Run:
$ tsrc --version\n
"},{"location":"getting-started/#creating_a_repository_for_the_manifest","title":"Creating a repository for the manifest","text":"Let's say you are working for the ACME company and you have many git repositories.
You need a tool to track them, so that if a new repository is created, all developers can get a clone on their development machine quickly, without having to look up its URL or even know it exists.
Also, you need to make sure the repos are cloned in a certain way, so that you can for instance refer a repo from an other one by using a relative path.
This is where tsrc
comes in.
The first step is to create a dedicated repository for the manifest. I know it may sound wasteful (\"I have already 100 repositories to manage, and you want me to create yet an other one?\"), but, trust me, it's worth it.
So, if your company uses a GitLab instance at gitlab.acme.com
and you want to crate a manifest for your team, you may start by creating a new repository at https://gitlab.acme.com/your-team/manifest
.
Inside this repository, create a file named manifest.yml
looking like this:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n\n - dest: bar\n url: git@gitlab.acme.com/your-team/bar\n
Note that this approach works with any king of Git Hosting system, not just a custom GitLab instance. Just replace gitlab.acme.com/your-team
with the correct suffix (like github.com/your-name/
if you want to track some repositories from your GitHub account).
Create a new, empty directory and then run tsrc init
from it, using the URL of the manifest created in the above step:
$ mkdir work\n$ cd work\n$ tsrc init git@gitlab.acme.com/your-team/manifest.git\n
You should see something like this:
:: Configuring workspace in /path/to/work\nCloning into 'manifest'...\n...\n=> Cloning missing repos\n* (1/2) Cloning foo\nCloning into 'foo'...\n...\n* (2/2) Cloning bar\nCloning into 'bar'...\n...\n=> Cloned repos:\n* foo cloned from gt@gitlab.acme.com/your-team/foo' (on master)\n* bar cloned from gt@gitlab.acme.com/your-team/bar' (on master)\n=> Configuring remotes\n=> Workspace initialized\n=> Configuration written in /path/to/work/.tsrc/config.yml\n
You will notice that:
foo
ad bar
repositories have been cloned into their respective destination/path/to/work/.tsrc/config.yml
. This file can be edited by hand to customize tsrc
behavior. Follow the relevant guide, or read the workspace configuration file reference for more details.Now let's assume that Alice created a new commit in foo
, and Bob a new commit it bar
, and that they both pushed them to the master
branch of the respective repositories.
Now that you have a workspace configured with tsrc
, you can use tsrc sync
to retrieve all the changes in one go:
$ cd work\n$ tsrc sync\n
This time, you should see the following output:
:: Using workspace in /path/to/work\n=> Updating manifest\n...\n=> Cloning missing repos\n=> Configuring remotes\n=> Synchronizing repos\n* (1/2) Synchronizing foo\n* Fetching origin\n...\n f20af74..aca6c35 master -> origin/master\n* Updating branch: master\nUpdating f20af74..aca6c35\nFast-forward\n new.txt | 1 +\n 1 file changed, 1 insertion(+)\n create mode 100644 new.txt\n* (2/2) Synchronizing bar\n* Fetching origin\n...\n f20af74..02cfef6 master -> origin/master\n* Updating branch: master\nUpdating f20af74..02cfef6\nFast-forward\n spam.py | 1 +\n 1 file changed, 1 insertion(+)\n create mode 100644 spam.py\n:: Workspace synchronized\n
Note: tsrc sync
does not call git pull
on every repository. The precise algorithm is described in the reference documentation
Let's say your team now needs a third repository (for instance, at gitlab.acme.com/your-team/baz
).
Start by making a commit in the manifest
repository that adds the new repository:
--- a/manifest.yml\n+++ b/manifest.yml\n@@ -1,2 +1,3 @@\n repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n\n - dest: bar\n url: git@gitlab.acme.com/your-team/baz\n\n+ - dest: baz\n+ url: git@gitlab.acme.com/your-team/baz\n
Then push this commit to the master
branch of the manifest.
This time when you run tsrc sync
:
manifest
repository will get updatedbaz
repo will be cloned in /path/to/work/baz
$ tsrc sync\n
:: Using workspace in /path/to/work\n=> Updating manifest\nremote: Enumerating objects: 5, done.\n...\nUnpacking objects: 100% (3/3), 354 bytes | 354.00 KiB/s, done.\nFrom gitlab.acme.com/your-team/manifest\n 63f12d4..bbcd4d9 master -> origin/master\nReset branch 'master'\n...\nHEAD is now at bbcd4d9 add baz\n\n=> Cloning missing repos\n* (1/1) Cloning baz\nCloning into 'baz'...\n...\nReceiving objects: 100% (3/3), done.\n=> Cloned repos:\n* baz cloned from git@gitlab.acme.com/bas (on master)\n=> Configuring remotes\n=> Synchronizing repos\n* (1/3) Synchronizing foo\n* Fetching origin\n* Updating branch: master\nAlready up to date.\n* (2/3) Synchronizing bar\n* Fetching origin\n* Updating branch: master\nAlready up to date.\n* (3/3) Synchronizing baz\n* Fetching origin\n* Updating branch: master\nAlready up to date.\n:: Workspace synchronized\n
"},{"location":"getting-started/#going_further","title":"Going further","text":"In this tutorial, we made a lot of assumptions:
master
as the main development branchgitlab.acme.com
in our example)tsrc
can handle all of this use cases, and more. See the other guides for more details.
All the development happens on GitHub.
You are free to open a pull request for anything you want to change on tsrc
.
In particular, pull requests that implement a prototype for a new feature are welcome, having \"real code\" to look at can provide useful insight, even if the code is not merged after all.
That being said, if you want your pull request to be merged, we'll ask that:
See the GitHub actions workflows to see what exactly what commands are run and the Python versions we support.
Also, if relevant, you will need to:
docs/changelog.md
)Finally, feel free to add your name in the THANKS
file ;)
$ poetry install\n
$ poetry run invoke lint\n$ poetry run pytest -n auto\n
"},{"location":"contrib/dev/#adding_documentation","title":"Adding documentation","text":"$ poetry run mkdocs serve\n
docs/
folder and review the changes in your browserReporting bugs and requesting new features is done one the tsrc issue tracker on GitHub.
"},{"location":"contrib/issues/#reporting_bugs","title":"Reporting bugs","text":"If you are reporting a bug, please provide the following information:
tsrc
versionDoing so will ensure we can investigate your bug right away.
"},{"location":"contrib/issues/#suggesting_new_features","title":"Suggesting new features","text":"If you think tsrc
is lacking a feature, please provide the following information:
Note that changingtsrc
behavior can get tricky.
First off, we want to avoid data loss following a tsrc
command above
Second, we want to keep tsrc
behavior as least surprising as possible, so that it can be used without having to read (too much of) documentation.
To that end, and keeping in mind tsrc
needs to accommodate a large variety of use cases, we want to keep the code:
The best way to achieve all of this is to keep it simple.
This means we'll be very cautious before implementing a new feature, so don't hesitate to open an issue for discussion before jumping into the development of a new feature.
"},{"location":"guide/ci/","title":"Using tsrc with Continuous Integration (CI)","text":""},{"location":"guide/ci/#github_actions","title":"GitHub Actions","text":"Let suppose you have a private GitHub organization holding several private repositories and tsrc to synchronize them using the SSH protocol. Let suppose you want to use GitHub Actions to download the code source of your organization, compile it and run some non regression tests. What to write to achieve this with tsrc?
"},{"location":"guide/ci/#step_1_your_tsrc_manifest","title":"Step 1: Your tsrc manifest","text":"Your tsrc manifest.yml
looks something like this:
repos:\n - url: git@github.com:project1/foo\n dest: foo\n
The git@
means SSH protocol.
In your private GitHub repository holding the GitHub workflows files, create the folder .github/workflows
and your yaml file with the desired name and the following content. For more information about GitHub actions syntax see this video:
name: tsrc with private github repos\non:\n workflow_dispatch:\n branches:\n - main\n\njobs:\n export_linux:\n runs-on: ubuntu-latest\n steps:\n - name: Installing tsrc tool\n run: |\n sudo apt-get update\n sudo apt-get install -y python3\n python -m pip install tsrc\n\n - name: Cloning private github repos\n run: |\n git config --global url.\"https://${{ secrets.ACCESS_TOKEN }}@github.com/\".insteadOf git@github.com:\n export WORKSPACE=$GITHUB_WORKSPACE/your_project\n mkdir -p $WORKSPACE\n cd $WORKSPACE\n tsrc init git@github.com:yourorganisation/manifest.git\n tsrc sync\n
This script will run on the latest Ubuntu Docker and triggers steps: - The first step named Installing tsrc tool
allows to install python3 and then tsrc. - The second step named Cloning private github repos
creates a folder named your_project
for your workspace and call the initialization and synchronization of your repositories.
The important command is:
git config --global url.\"https://${{ secrets.ACCESS_TOKEN }}@github.com/\".insteadOf git@github.com:\n
which allows to replace the SSH syntax by the HTTPs syntax on your GitHub repository names.
"},{"location":"guide/ci/#step_3_create_the_github_secret","title":"Step 3: Create the GitHub secret","text":"For GitHub organization one member of the team has the responsibility to hold a Personal access tokens
for the organization. Go https://github.com/settings/tokens and click on the button Generate new token
then click on repo
checkbox then click on the button Generate token
.
Now, this token shall be saved into an action secret named ACCESS_TOKEN
inside the GitHub repository holding the GitHub workflows files.
In the menu Actions
of your repository you can trig the workflow. In this example we used workflow_dispatch
to perform manual triggers. So click on the button to start the process. Once this step done with success, you can update your workflow yaml to complete your CI work: compilation of your project, run non regression tests, etc.
By default, tsrc sync
synchronize projects using branches names.
Usually, one would use the same branch name for several git repositories, like this:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n branch: main\n\n - dest: bar\n url: git@gitlab.acme.com/your-team/bar\n branch: main\n
The assumption here is that foo
and bar
evolve \"at the same time\", so when the main
branch of foo
is updated, the main
branch of bar
much change too.
Sometimes though, this will not be the case. For instance, the main
branch of the bar
repo needs a specific, fixed version of foo
in order to work.
One way to solve this is to push a v1.0 tag in the foo
repository, and change the manifest too look like this:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n- branch: main\n+ tag: v1.0\n
"},{"location":"guide/fixed-refs/#using_a_sha1","title":"Using a sha1","text":"An other way is to put the SHA1 of the relevant git commit in the foo
repository in the manifest:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n branch: main\n+ sha1: ad2b68539c78e749a372414165acdf2a1bb68203\n
"},{"location":"guide/fixed-refs/#cloning_repos_using_fixed_refs","title":"Cloning repos using fixed refs","text":"tsrc
will call git clone --branch <tag>
(which is valid)tsrc
will call git clone
, followed by git reset --hard <sha1>
This is because you cannot tell git to use an arbitrary git reference as start branch when cloning (tags are fine, but sha1s are not).
This also explain why you need both branch
and sha1
in the configuration.
Here's what tsrc sync
will do when trying to synchronize a repo configured with a fixed ref:
git fetch --tags --prune
git reset --hard <tag or sha1>
tsrc
comes with a foreach
command that allows you to run the same command for each repo in the workspace.
This can be used for several things. For instance, if you are building an artifact from a group of repositories, you may want to put a tag on each repo that was used to produce it:
$ tsrc foreach git tag v1.2\n
:: Using workspace in /path/to/work\n:: Running `git tag v1.1` on 2 repos\n/path/to/work/foo $ git tag v1.2\n/path/to/work/bar $ git tag v1.2\n/path/to/work/baz $ git tag v1.2\nOK \u2713\n
"},{"location":"guide/foreach/#caveats","title":"Caveats","text":"-
: you need to call foreach
like this:$ tsrc foreach -- some-command --with-option\n
-c
option:$ tsrc foreach -c 'echo $PWD'\n
Note that we need single quotes here to prevent the shell from expanding the PWD
environment variable when tsrc
is run.
The current tsrc
implementation may not contain all the features your organization needs.
The good news is that you can extend tsrc
's feature set by using tsrc foreach
.
Let's take an example, where you have a manifest containing foo
and bar
and both repos are configured to use a master
branch.
Here's what happens if you run tsrc sync
with bar
on the correct branch (master
), and foo
on an incorrect branch (devel
):
$ tsrc sync\n
:: Using workspace in /path/to/work\n=> Updating manifest\n...\n=> Cloning missing repos\n=> Configuring remotes\n=> Synchronizing repos\n* (1/2) Synchronizing foo\n* Fetching origin\n* Updating branch: devel\nUpdating 702f428..2e4fb45\nFast-forward\n...\n* (2/2) Synchronizing bar\n* Fetching origin\n* Updating branch: master\nAlready up to date.\nError: Failed to synchronize the following repos:\n* foo : Current branch: 'devel' does not match expected branch: 'master'\n
If this happens with multiple repos, you may want a command to checkout the correct branch automatically.
Here's one way to do it:
$ tsrc foreach -c 'git checkout $TSRC_PROJECT_MANIFEST_BRANCH'\n
Here we take advantage of the fact that tsrc
sets the TSRC_PROJECT_MANIFEST_BRANCH
environment variable correctly for each repository before running the command.
Here's the whole list:
Variable DescriptionTSRC_WORKSPACE_PATH
Full path of the workspace root TSRC_MANIFEST_BRANCH
Branch of the manifest TSRC_MANIFEST_URL
URL of the manifest TSRC_PROJECT_CLONE_URL
URL used to clone the repo TSRC_PROJECT_DEST
Relative path of the repo in the workspace TSRC_PROJECT_MANIFEST_BRANCH
Branch configured in the manifest for this repo TSRC_PROJECT_REMOTE_<NAME>
URL of the remote named 'NAME' TSRC_PROJECT_STATUS_DIRTY
Set to true
if the project is dirty, otherwise unset TSRC_PROJECT_STATUS_AHEAD
Number of commits ahead of the remote ref TSRC_PROJECT_STATUS_BEHIND
Number of commits behind the remote ref TSRC_PROJECT_STATUS_BRANCH
Current branch of the repo TSRC_PROJECT_STATUS_SHA1
SHA1 of the current branch TSRC_PROJECT_STATUS_STAGED
Number of files that are staged but not committed TSRC_PROJECT_STATUS_NOT_STAGED
Number of files that are changed but not staged TSRC_PROJECT_STATUS_UNTRACKED
Number of files that are untracked You can implement more complex behavior using the environment variables above, for instance:
#!/bin/bash\n# in switch-and-pull\nif [[ \"${TSRC_PROJECT_STATUS_DIRTY}\" = \"true\" ]]; then\n echo Error: project is dirty\n exit 1\nfi\n\ngit switch $TSRC_PROJECT_MANIFEST_BRANCH\ngit pull\n
$ tsrc foreach switch-and-pull\n:: Running `switch-and-pull` on 2 repos\n* (1/2) foo\n/path/to/foo $ switch-and-pull\nSwitched to branch 'master'\nYour branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\nUpdating 9e7a8e4..5f9bbd4\nFast-forward\n* (2/2) bar\n/path/to/bar $ switch-and-pull\nError: project is dirty\nError: Command failed for 1 repo(s)\n* bar\n
Of course, feel free to use your favorite programming language here :)
"},{"location":"guide/fs/","title":"Performing file system operations","text":""},{"location":"guide/fs/#introduction","title":"Introduction","text":"When using tsrc
, it is assumed that repositories are put in non-overlapping file system hierarchies, like this:
workspace/\n project_1/\n CMakeLists.txt\n foo.cpp\n bar.cpp\n project_2/\n CMakeLists.txt\n spam.cpp\n eggs.cpp\n
Not like that, where project_2
is inside a sub-directory of project_1
:
workspace/\n project_1/\n CMakeLists.txt\n foo.cpp\n bar.cpp\n project_2/\n CMakeLists.txt\n spam.cpp\n eggs.cpp\n
Note
if you really need project_2
to be a sub-directory of project_1
, consider using git submodules instead.
This is usually fine, except when project_1
and project_2
share some common configuration.
For instance, you may want to use clang-format
for both project_1
and project_2
.
One solution is to put the .clang-format
configuration file in a repo named common
and then tell tsrc
to copy it at the root of the workspace:
repos:\n - dest: project_1\n url: git@acme.com:team/project_1\n\n - dest: project_2\n url: git@acme.com:team/project_2\n\n - dest: common\n url: git@acme.com:team/commont\n copy:\n - file: clang-format\n dest: .clang-format\n
$ tsrc sync\n=> Cloning missing repos\n* (1/1) Cloning common\nCloning into 'common'...\n...\n=> Performing filesystem operations\n* (1/1) Copy /path/to/work/common/clang-format -> /path/to/work/.clang-format\n
Notes:
copy
only works with files, not directories.The above method works fine if the file does not change too often - if not, you may want to create a symbolic link instead:
repos:\n - dest: project_1\n url: git@acme.com:team/project_1\n\n - dest: project_2\n url: git@acme.com:team/project_2\n\n - dest: common\n url: git@acme.com:team/commont\n symlink:\n - source: .clang-format\n target: common/clang-format\n
$ tsrc sync\n=> Cloning missing repos\n...\n=> Performing filesystem operations\n* (1/1) Lint /path/to/work/.clang-format -> common/.clang-format\n
Notes:
The source path for a symbolic link is relative to the top-level <workspace>
, whereas each target path is then relative to the associated source. (This path relationship is essentially identical to how ln -s
works on the command line in Unix-like environments.) Multiple symlinks can be specified; each must specify a source and target.
Symlink creation is supported on all operating systems, but creation of NTFS symlinks on Windows requires that the current user have appropriate security policy permission (SeCreateSymbolicLinkPrivilege). By default, only administrators have that privilege set, although newer versions of Windows 10 support a Developer Mode that permits unprivileged accounts to create symlinks. Note that Cygwin running on Windows defaults to creating links via Windows shortcuts, which do not require any special privileges. (Cygwin's symlink behavior can be user controlled with the winsymlinks
setting in the CYGWIN
environment variable.)
Sometimes it can be necessary to create groups of repositories, especially if the number of repositories grows and if you have people in different teams work on different repositories.
"},{"location":"guide/groups/#defining_groups_in_the_manifest","title":"Defining groups in the manifest","text":"The first step is to edit the manifest.yml
file to describe the groups. Here's an example.
repos:\n - {url: git@gitlab.local:acme/one, dest: one}\n - {url: git@gitlab.local:acme/two, dest: two}\n - {url: git@gitlab.local:acme/three, dest: three}\n\ngroups:\n default:\n repos: []\n g1:\n repos:\n - one\n - two\n g2:\n repos:\n - three\n
Here we define a g1
group that contains repositories named one
and two
, and a g2
group that contains the repository named three
.
tsrc init
","text":"If you only need the repositories in the g1
group you can run:
tsrc init git@gitlab.local:acme/manifest --group g1\n
"},{"location":"guide/groups/#filtering_repositories_in_groups_with_regular_expressions","title":"Filtering repositories in groups with regular expressions","text":"You can utilize inclusive regular expression with the -r
-flag and exclusive regular expression with the -i
-flag. This allows you to filter repositories within a group or a set of groups for the given action.
To include all repositories in the group g1 matching \"config\" and excluding \"template\", you can do the following:
tsrc init git@gitlab.local:acme/manifest --group g1 -r config -i template\n
"},{"location":"guide/groups/#updating_workspace_configuration","title":"Updating workspace configuration","text":"Alternatively, you can edit the .tsrc/config.yml
file, like this:
manifest_url: git@gitlab.local:acme/manifest.git\nmanifest_branch: master\nrepo_groups:\n- g1 # <- specify the list of groups to use\n
You can use this technique to change the groups used in a given workspace - the above method using init
only works to create new workspaces.
The config file contains other configuration options, which are described in the workspace configuration documentation.
"},{"location":"guide/manifest/","title":"Editing the manifest safely","text":""},{"location":"guide/manifest/#introduction_when_things_go_wrong","title":"Introduction: when things go wrong","text":"Let's assume you've successfully implemented tsrc
for your organization - now need to make sure to not break anyone's workflow.
Let's see what could go wrong if you make mistakes while editing the manifest, using a branch called broken
for the sake of the example).
First, let's see what happens if you break the YAML syntax:
commit 1633c5a6 (HEAD -> broken, origin/broken)\n\n Break the manifest syntax\n\ndiff --git a/manifest.yml b/manifest.yml\nindex fe74142..068c35e 100644\n--- a/manifest.yml\n+++ b/manifest.yml\n@@ -1,4 +1,4 @@\n-repos:\n+repos\n - url: git@github.com:your-tools/bar.git\n dest: bar\n
After this change is push, anyone using the broken
branch of the manifest will be faced with this kind of error message:
$ tsrc sync\n
=> Updating manifest\nReset branch 'broken'\nYour branch is up to date with 'origin/broken'.\nBranch 'broken' set up to track remote branch 'broken' from 'origin'.\nHEAD is now at 1633c5a Break the manifest syntax\nError: /path/to/work/.tsrc/manifest/manifest.yml: mapping values are\nnot allowed here :\n\n - url: git@gitlab.acme.com:your-team/foo\n ^ (line: 2)\n
Similarly, if you put an invalid URL in the manifest, like this:
commit ccfb902 (HEAD -> broken, origin/broken)\n\n Use invalid URL for bar repo\n\ndiff --git a/manifest.yml b/manifest.yml\nindex fe74142..068c35e 100644\n--- a/manifest.yml\n+++ b/manifest.yml\n@@ -1,4 +1,4 @@\nrepos:\n- - url: git@gitlab.acme.com:your-team/bar\n+ - url: git@gitlab.acme.com:your-team/invalid\n dest: bar\n
Users will get:
$ tsrc sync\n
:: Using workspace in /path/to/work\n=> Updating manifest\n...\nHEAD is now at ccfb902 Use invalid URL\n=> Cloning missing repos\n=> Configuring remotes\n* bar: Update remote origin to new url: (git@acme.com:your-team/invalid.git)\n...\n=> Synchronizing repos\n* (1/2) Synchronizing bar\n* Fetching origin\nERROR: Repository not found.\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\nError: fetch from 'origin' failed\n* (2/2) Synchronizing foo\n ...\nError: Failed to synchronize the following repos:\n* bar : fetch from 'origin' failed\n
This will probably not be a huge problem for you, dear reader, because you know about tsrc's manifest and its syntax.
It will, however, be a problem for people who are just using tsrc
without knowledge of how it is implemented, because those error messages will definitely confuse them.
If you have a file on your machine containing the manifest changes, you can use tsrc apply-manifest
to check those changes against your own workspace:
$ cd /path/to/work\n$ tsrc apply-manifest /path/to/manifest-repo/manifest.yml\n# Check that the changes are OK\n# If so, commit and push manifest changes:\n$ cd path/to/manifest-repo\n$ git commit -a -m \"...\"\n$ git push\n# Now you know that everyone can safely run `tsrc sync`\n
"},{"location":"guide/manifest/#additional_notes","title":"Additional notes","text":"It is not advised to edit the file in .tsrc/manifest/manifest.yml
directly, because tsrc sync
will silently undo any local changes made to this file. This is a known bug, see #279 for details.
It is common to place the manifest repo itself in the manifest - so it's easy to edit or read:
# In acme.com:your-team/manifest - manifest.yml\nrepos:\n - url: git@acme.com:your-team/manifest\n dest: manifest\n\n - url: git@acme.com:your-team/foo\n dest: foo\n\n - url: git@acme.com:your-team/bar\n dest: bar\n
In that case, you would use:
$ tsrc apply-manifest <workspace>/manifest/manifest.yml\n
to check changes before pushing them.
"},{"location":"guide/remotes/","title":"Using several remotes","text":"When you specify a repository in the manifest with just an URL, tsrc
assumes you want a remote named origin:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n\n - dest: bar\n url: git@gitlab.acme.com/your-team/bar\n
But sometimes you need several remotes. Let's see a few use cases.
"},{"location":"guide/remotes/#mirroring_open-source_projects","title":"Mirroring open-source projects","text":"If you want some repos in your organization to be open source, you may need:
In that case, you can use an alternative syntax:
repos:\n # foo is open source and thus needs two remotes:\n - dest: foo\n - remotes:\n - name: origin\n url: git@gitlab.acme.com/your-team/foo\n - name: github\n url: git@github.com/your-team/foo\n\n # bar is closed source and thus only needs the\n # default, 'origin' remote:\n - dest: bar\n url: gitlab.acme.com/your-team/bar\n
After this change, when running tsrc init
or tsrc sync
, both the origin
and github
remotes will be created in the foo
repo if they don't exist, and both remotes will be fetched when using tsrc sync
.
Sometimes you will need two remotes, because depending the physical location of your developers, they need to use either:
In that case, you can create a manifest looking like this:
repos:\n - dest: foo\n - remotes:\n - name: origin\n url: git@gitlab.local/your-team/foo\n - name: vpn\n url: git@myvpn.com/gitlab/your-team/foo\n\n - dest: bar\n - remotes:\n - name: origin\n url: git@gitlab.local/your-team/bar\n - name: vpn\n url: git@myvpn.com/gitlab/your-team/bar\n
Developers can then use the -r, --singular-remote
option to either use the origin
or vpn
when running tsrc init
(to create a workspace), or tsrc sync
(to synchronize it), depending on their physical location:
# Init the workspace using the 'vpn' remote\n$ tsrc init -r vpn\n# Bring back the computer in the office\n# Synchronize using the 'origin' remote:\n$ tsrc sync -r origin\n
Note
When using this option, tsrc
expects the remote to be present in the manifest for all repositories.
The configuration file created by tsrc init
contains the whole list of available settings, with their default value, and is located at </path/to/workspace/.tsrc/manifest.yml>
.
Note that if you use command-line options when using tsrc init
, those will be written in the .tsrc/config.yml
.
For instance:
tsrc init git@github.com:dmerejkowsky/dummy-manifest\n
generates this file:
manifest_url: git@github.com:dmerejkowsky/dummy-manifest\nmanifest_branch: master\nrepo_groups: []\nshallow_clones: false\nclone_all_repos: false\nsingular_remote:\n
But
tsrc init git@github.com:dmerejkowsky/dummy-manifest --branch main\n
generates this instead:
manifest_url: git@github.com:dmerejkowsky/dummy-manifest\nmanifest_branch: main\nrepo_groups: []\nshallow_clones: false\nclone_all_repos: false\nsingular_remote:\n
"},{"location":"guide/workspace-config/#editing","title":"Editing","text":"You can edit the workspace configuration as you please, for instance if you need to switch the manifest branch.
If you do so, note that your changes will be taken into account next time you run tsrc sync
.
We use the argparse library to parse command line arguments, so the --help
messages are always up-to-date, probably more so than this documentation :)
tsrc
uses the same \"subcommand\" pattern as git does.
Options common to all commands are placed right before the command name.
Options after the command name only apply to this command.
For instance:
$ tsrc --verbose sync\n$ tsrc init MANIFEST_URL\n
"},{"location":"ref/cli/#goodies","title":"Goodies","text":"First, note that like git
, tsrc will walk up the folders hierarchy looking for a .tsrc
folder, which means you can run tsrc commands anywhere in your workspace, not just at the top.
Second, almost all commands run the operation in parallel. For instance, tsrc sync
by default will use as many jobs as the number of CPUs available on the current machine to synchronize the repos in your workspace. If this behavior is not desired, you can specify a greater (or lower) number of jobs using something like tsrc sync -j2
, or disable the parallelism completely with -j1
. You can also set the default number of jobs by using the TSRC_PARALLEL_JOBS
environment variable.
Initializes a new workspace.
MANIFEST_URL should be a git URL containing a valid manifest.yml
file.
The -g,--groups
option can be used to specify a list of groups to use when cloning repositories.
The -r
\"inclusive regular expression\" and -i
\"exclusive regular expression\" options can be combined with the group option to filter for repositories within a group. -r
takes precedence if both options are present.
The -s,--shallow
option can be used to make shallow clone of all repositories.
If you want to add or remove a group in your workspace, you can edit the configuration file in <workspace>/.tsrc/config.yml
The -r,--singular-remote
option can be used to set a fixed remote to use when cloning and syncing the repositories. If this flag is set, the remote from the manifest with the given name will be used for all repos. It is an error if a repo does not have this remote specified.
Runs command --opt1 arg1
in every repository, and report failures at the end.
Note the --
token to separate options for command
from options for tsrc
.
/bin/sh
on Linux or macOS, cmd.exe
on Windows). tsrc log --from FROM [--to TO] Display a summary of all changes since FROM
(should be a tag), to TO
(defaulting to master
).
Note that if no changes are found, the repository will not be displayed at all.
tsrc statusDisplays a summary of the status of your workspace:
--correct-branch
/-c
flag is set, then the branch is changed to the configured one and then the repository is updated. Otherwise that repository will not be not updated. tsrc version Displays tsrc
version number, along additional data if run from a git clone. tsrc apply-manifest PATH Apply changes from the manifest file located at PATH
. Useful to check changes in the manifest before publishing them to the manifest repository."},{"location":"ref/manifest-config/","title":"Manifest configuration","text":"The manifest configuration must be stored in a file named manifest.yml
, using YAML syntax.
It is always parsed as a mapping. Here's an example:
repos:\n - url: git@gitlab.local:proj1/foo\n dest: foo\n branch: next\n\n - remotes:\n - name: origin\n url: git@gitlab.local:proj1/bar\n - name: upstream\n url: git@github.com:user/bar\n dest: bar\n branch: master\n sha1: ad2b68539c78e749a372414165acdf2a1bb68203\n\n - url: git@gitlab.local:proj1/app\n dest: app\n tag: v0.1\n copy:\n - file: top.cmake\n dest: CMakeLists.txt\n - file: .clangformat\n symlink:\n - source: app/some_file\n target: ../foo/some_file\n
In this example:
proj1/foo
will be cloned into <workspace>/foo
using the next
branch.proj1/bar
will be cloned into <workspace>/bar
using the master
branch, and reset to ad2b68539c78e749a372414165acdf2a1bb68203
.proj1/app
will be cloned into <workspace>/app
using the v0.1
tag,top.cmake
will be copied from proj1/app/top.cmake
to <workspace>/CMakeLists.txt
,.clang-format
will be copied from proj1/app/
to <workspace>/
, and<workspace>/app/some_file
to <workspace>/foo/some_file
.repos
(required): list of repositories to clonegroups
(optional): list of groupsEach repository is also a mapping, containing:
url
if you just need one remote named origin
name
and url
. In that case, the first remote will be used for cloning the repository.dest
(required): relative path of the repository in the workspacebranch
(optional): The branch to use when cloning the repository (defaults to master
)tag
(optional):tsrc init
: Project will be cloned at the provided tag.tsrc sync
: If the project is clean, project will be reset to the given tag, else a warning message will be printed.sha1
(optional):tsrc init
: Project will be cloned, and then reset to the given sha1.tsrc sync
: If the project is clean, project will be reset to the given sha1, else a warning message will be printed.ignore_submodules
(optional, default=false
):tsrc init
: if ignore_submodules
is true
, do not recursively clone submodules.tsrc sync
: if ignore_submodules
is true
, do not initialize or update submodules. to the given sha1, else a warning message will be printed.copy
(optional): A list of mappings with file
and dest
keys.symlink
(optional): A list of mappings with source
and target
keys.See the Using fixed references and the Performing file system operations guides for details about how and why you would use the tag
, sha1
, copy
or symlink
fields.
The groups
section lists the groups by name. Each group should have a repos
field containing a list of repositories (only repositories defined in the repos
section are allowed).
The groups can optionally include other groups, with a includes
field which should be a list of existing group names.
The group named default
, if it exists, will be used to know which repositories to clone when using tsrc init
and the --group
command line argument is not used.
Example:
repos:\n - dest: a\n url: ..\n - dest: b\n url: ..\n - dest: bar\n url: ..\n - dest: baz\n url: ..\n\ngroups:\n default:\n repos: [a, b]\n foo:\n repos: [bar, baz]\n includes: [default]\n
$ tsrc init <manifest_url>\n# Clones a, b\n$ tsrc init <manifest_url> --group foo\n# Clones a, b, bar and baz\n
Note that tsrc init
records the names of the groups it was invoked with, so that tsrc sync
re-uses them later on. This means that if you want to change the groups used, you must re-run tsrc init
with the new group list.
Note
More information about how to use groups is available in the relevant guide.
"},{"location":"ref/sync/","title":"Sync algorithm","text":"You may have noticed that tsrc sync
does not just calls git pull
on every repository.
Here's the algorithm that is used:
git fetch --tags --prune
--correct-branch
flag is set and the repository is clean, the branch is changed to the configured one. Note that:
git fetch
is always called so that local refs are up-to-datetsrc
will simply print an error and move on to the next repository if the fast-forward merge is not possible. That's because tsrc
cannot guess what the correct action is, so it prefers doing nothing. It's up to the user to run something like git merge
or git rebase
.The workspace configuration lies in <workspace>/.tsrc/config.yml
. It is created by tsrc init
then read by tsrc sync
and other commands. It can be freely edited by hand.
Here's an example:
manifest_url: git@acme.corp:manifest.git\nmanifest_branch: master\nshallow_clones: false\nrepo_groups:\n- default\nclone_all_repos: false\nsingular_remote:\n
manifest_url
: an git URL containing a manifest.yml
filemanifest_branch
: the branch to use when updating the local manifest (e.g, the first step of tsrc sync
)shallow_clones
: whether to use only shallow clones when cloning missing repositoriesrepo_groups
: the list of groups to use - every mentioned group must be present in the manifest.yml
file (see above)clone_all_repos
: whether to ignore groups entirely and clone every repository from the manifest insteadsingular_remote
: if set to <remote-name>
, behaves as if tsrc sync
and tsrc init
were called with --singular-remote <remote-name>
option. See the Using remotes guide for details.tsrc
is a command-line tool that helps you manage groups of git repositories.
It works by listing the repositories in a file called manifest.yml
that looks like this:
repos:\n - dest: foo\n url: git@example.com:foo.git\n\n - dest: bar\n url: git@example.com:bar.git\n
You can then use:
tsrc init <manifest url>
to create a workspace containing the foo
and bar
repository
tsrc sync
to synchronize all repos in the workspace.
... and many more commands. Run tsrc help
to list them, or read the command line reference
Interested in using tsrc
in your own organization?
Proceed to the getting started tutorial!
"},{"location":"#guides","title":"Guides","text":"Once you've learn how to setup tsrc for your organization, feel free to read the following guides - tsrc supports a variety of use cases beyond just listing git repositories to be cloned or synchronized and are described here:
tsrc
now makes sure repos included via groups are processed before the other repos. See #356 for details. Thanks to @raabf for the bug report and code review!tsrc init
: fix order of operations - clone the local manifest before writing the workspace configuration. Fixes #344, where users could not run init
a second time if the previous call failed. Bug report by @cgestes.tsrc init
: do not assume the default branch of the manifest is master
. Note that master
is still hard-coded in a few places. See #347 for details.-j 1
, do not sort repositories by lexical order of destination, but preserve the order in which they were specified in the manifest. Suggested by @raabf.python -m tsrc
in addition to just tsrc
ignore_submodules
repository option - Patch by Thomas Hiscock.-j
is not used, try getting the default jobs
value from the TSRC_PARALLEL_JOBS
environment variable. Patch by Marcin Jaworski.attr
tsrc sync
now uses parallel jobs by default. Use -j1
to force sequential processing. Patch by @gdubickiAll of tsrc
commands can now be run in parallel. Try for instance tsrc sync -j auto
.
tsrc foreach
now sets a bunch of environment variables. This allows developers to add new behaviors to tsrc without having to change its source code. See the relevant guide for more information.
Augment documentation with more use cases and examples (still a work in progress)
Remove tsrc version
- Use tsrc --version
instead.
The 'parallel' feature caused the output of some commands like foreach
or log
to change slightly. Hopefully tsrc
output is now more consistent.
Fix crash when running tsrc
without any arguments
Fix crash when trying to clone repositories in some rare corner cases (like the destination existing but not being a directory)
tsrc
imports consistentrepr
on tsrc
Errors.tsrc apply-manifest
now performs file system operationblack
, mypy
, isort
...)copier
to simplify maintenance of tools configurationtsrc
calls git clone
with --recurse-submodules
when adding missing repositoriestsrc
calls git submodule update --init --recursive
when updating repositoriesmain
.Project has been moved from TankerHQ
organization to dmerejkowsky
. New urls are:
Add CI jobs to check this project also works with Python 3.9
Path Pie
dependencytsrc sync
and tsrc init
can now create symlinks as specified in the manifest file:
repos:\n\n - url: git@gitlab.local:proj1/app\n dest: app\n symlink:\n - source: app/some_file\n target: ../some_file\n
In this case, a symlink will be created from <workspace>/app/some_file
to <workspace>/some_file
. (both source
and target
keys are relative to the repository's destination).
--group
option and the --all-cloned
options--groups-from-config
options since this is now the default behaviorlint.sh
It was discovered that the manifest syntax was confusing for newcomers, so we decided to update it.
In particular, the src
key meant both a relative path in the workspace when used in the repo
config, and a relative path in the a repository when using in the repo.copy
config.
Starting with this release, repo.src
becomes repo.dest
and repo.copy.src
becomes repo.copy.file
.
# Before (tsrc < 2.1.0)\nrepos:\n url: \"https://acme.corp/foo\"\n src: foo\n copy:\n src: some-file\n dest: some-file\n
# After (tsrc >= 2.1.0)\nrepos:\n url: \"https://acme.corp/foo\"\n dest : foo\n copy:\n file: some-file\n dest: some-file\n
This should make it clearer what tsrc
does because:
dest
now always refers to a relative path in the workspace (both in repo
and copy
).repo.copy.file
it's obvious that tsrc
only supports copying files, not directories.Drop support for Python 3.5
"},{"location":"changelog/#new_features","title":"New features","text":"tsrc init
learned a -r, --remote
option that pins the remote with the given name as the only remote to be used for cloning and syncing. tsrc
expects this remote to be present in the manifest for all repositories. This is useful if you use the same workspace in different physical locations, and one of the remotes is behind a VPN for instance. Patch by @tronje.copy
statements in repos
libgit2
instead of running git commands in the tests helpers).safety
path
library.Remove the tsrc push
command and all review automation features. Please use hub, lab, or repo instead. See #207 for the discussion leading to this removal.
Implement small improvements on tsrc
output messages.
tsrc apply-manifest
, to apply changes in a manifest file locally, without having to make a commit and push to a server first.python_requires
value in project metadatatsrc init
with a list of groups.Starting the new year with a stable release, at last!
"},{"location":"changelog/#revamp_group_ux","title":"Revamp group UX","text":"The changes below in the configuration file and command line syntax allow for better UX regarding groups. See the corresponding milestone for the full list.
"},{"location":"changelog/#new_configuration_file","title":"New configuration file","text":"Previously, tsrc
stored its permanent configuration in .tsrc/manifest.yml
and the file was not supposed to be edited by hand. Instead, users could use tsrc init
to modify it, for instance with the --branch
argument.
Starting with this release, the command tsrc init
can only be run once per workspace, and you must edit the .tsrc/config.yml
file instead.
tsrc init
: remove --file
option.tsrc foreach
: instead of repeating the --group
option, you can use --groups
with a list of groups:# before\ntsrc init --group foo --group bar\n\n# after\ntsrc init --groups foo bar\n
tsrc init
learned a --clone-all-repos
option to clone all repositories from the manifest, regardless of the groups. Fix #181
Remove --file
option from tsrc init
.
tsrc foreach
learned a --groups-from-config
option to use the groups configured in the workspace. Fix #178, #179.
tsrc push
learned a -o, --origin
option to specify a remote name different from \"origin\". Fix #170
tsrc push --approvers
on GitLab Community Edition. (#165)master
branch.tsrc status
: add information when local branch does not match manifest configuration. (#190). Feature suggested by @janjachnicktsrc push --reviewer
on GitLab Community Edition)tsrc foreach
fails to start the process. Suggested by @dlewis-ald in #163tsrc
configuration file. Fix #158tsrc status
on a workspace with missing repositories (#160) - reported by @blastrocktsrc sync --force
. Currently all it does is running git fetch --force
on all repositories. Use with caution. See #152 for details.tsrc sync
when the repo
configuration in the manifest contained neither an URL nor a remote. tsrc
now aborts as soon as the misconfiguration of the manifest is detected (Reported by @jongep86)--file
option to tsrc init
so that manifest can be read from a custom path in the file systemxdg
to pyxdg
codecov.io
to measure coveragepython-cli-ui
.TankerHQ
Fix crash when using tsrc push
on a GitHub repository for the first time.
Fix weird output when configuring remotes.
"},{"location":"changelog/#v060_2018-10-09","title":"v0.6.0 (2018-10-09)","text":""},{"location":"changelog/#add_support_for_multiple_remotes","title":"Add support for multiple remotes","text":"# still valid (implicit 'origin' remote)\nsrc: foo\nurl: git@github.com/foo\n\n# also valid (two explicit remotes)\nsrc: foo\nremotes:\n - { name: origin, url: git@github.com:john/foo }\n - { name: upstream, url: git@github.com:foo/foo}\n\n# not valid (ambiguous)\nsrc: foo\nurl: git@github.com:john/foo\nremotes:\n - { name: upstream, url: git@github.com:foo/foo }\n
Thanks @tst2005 and @cgestes for their help with the configuration format.
"},{"location":"changelog/#tsrc_foreach","title":"tsrc foreach","text":"tsrc foreach
: add a --group
option to select the repositories to run the command on. Fix #40-r,--approvers
option in tsrc push
(GitLab Enterprise Edition only).tsrc push
was no longer able to create a merge request on GitLab if --target
was not set.tsrc push
: new features and bug fixesSee below for the details.
"},{"location":"changelog/#preliminary_github_support","title":"Preliminary GitHub support","text":"tsrc
from a repository which has a URL starting with git@github.com
.tsrc
will prompt you once for your login and password and then store an API token.
Afterwards, you'll be able to use tsrc
push to:
-a/--assignee
option)--reviewers
option)--merge
option)This change has no impact if you were already using GitLab
.
tsrc push
: new features and bug fixes","text":"--close
option.-m/--message
option is gone, use --title
instead. There's a concept of \"description\" or \"message\" for pull requests and merge requests, but the value of the option was only used to update the title, so it had to be renamed.tsrc push <local>:<remote>
to explicitly specify local and remote branch names.tsrc push
(see issue #80). Patch by @maximerety.Breaking change: Instead of using fixed_ref
in the manifest, you should now use tag
or sha1
:
old:
repos:\n - src: git@example.com/foo\n fixed_ref: 42a70\n
new:
repos:\n - src: git@example.com/foo\n tag: v0.1\n
See the dedicated section about manifest format and the #57 pull request discussion for the details.
This allow us to implement different behaviors depending on whether or not the fixed ref is a tag or just a sha1.
"},{"location":"changelog/#support_for_shallow_clones","title":"Support for shallow clones","text":"To save time and space, you can use tsrc init --shallow
to only have shallow clones in your workspace.
Note that due to limitations in git
itself, the shallow
option cannot be used with a fixed SHA1. If you need this, prefer using a tag
instead.
Organization TankerApp
was renamed to TankerHQ
. New urls are:
We now use pipenv for dependency handling.
tsrc status
to handle tags. Patch by @arnaudgelas.tsrc version
.tsrc status
output. Now also shows number of commits ahead and behind, and display a short SHA-1 when not on any branch. Initial patch by @arnaudgelas.Breaking change: Add support for groups (#30). Reported by @arnaudgelas.
See the dedicated section about manifest format for details.
Upgrading from v0.2.4:
To upgrade from an older version of tsrc
, you should re-run tsrc init
with the correct url:
# Check manifest URL:\n$ cd <workspace>/.tsrc/manifest\n$ git remote get-url origin\n# Note the url, for instance ssh://git@example.com:manifest.git\n$ cd <workspace>\n$ tsrc init <manifest-url>\n
This is required to create the <workspace>/.tsrc/manifest.yml
file which is later used by tsrc sync
and other commands.
tsrc push --assignee
: fix when there are more than 50 GitLab users (#25). Reported by @arnaudgelasSplit user interface functionality into its own project: python-cli-ui.
Add --quiet
and --color
global options.
Bug fix release.
tsrc init
: Fix crash when a repository is empty (#17). Reported by @nicolasbrechettsrc push
: Fix rude message when credentials are missing (#20). Reported by @cgestesPackaging fixes.
"},{"location":"changelog/#v020_2017-08-09","title":"v0.2.0 (2017-08-09)","text":"New syntax is:
repos:\n - src: foo\n url: git@gitlab.com:proj/foo\n branch: next\n\n - src: bar\n url: git@gitlab.com:proj/bar\n branch: master\n fixed_ref: v0.1\n
Note that branch
is still required.
dest
part of the copy
section if src
and dest
are equal:copy:\n - src:foo\n\n# same thing as\ncopy:\n - src: foo\n dest: foo\n
"},{"location":"changelog/#v014_2017-08-04","title":"v0.1.4 (2017-08-04)","text":"Support for Python 3.3, 3.4, 3.5 and 3.6
"},{"location":"changelog/#v011_2017-08-02","title":"v0.1.1 (2017-08-02)","text":"First public release
"},{"location":"code-manifesto/","title":"Code Manifesto","text":""},{"location":"code-manifesto/#basics","title":"Basics","text":"We use black
to enforce a coding style matching PEP8.
In addition, every text file must be pushed using UNIX line endings. (On Windows, you are advised to set core.autocrlf
to true
in your git config file.)
# Yes\ndef bar():\n \"\"\" bar stuff \"\"\"\n a = \"foo\"\n\n\n# No\ndef bar():\n ''' bar stuff '''\n a = 'foo'\n\n# Exception\nmy_str = 'It contains some \"quotes\" inside'\n
Use the fact that empty data structures are falsy:
# Yes\nif not errors:\n ...\n# No\nif len(errors) == 0:\n ...\n
Avoid using double negatives:
# Yes\ndef make_coffee(sugar=False):\n if sugar:\n print(\"with sugar\")\n\n# No\ndef make_coffee(without_sugar=True):\n if not without_sugar:\n print(\"with sugar\")\n
Prefer using \"f-strings\" if possible, +
may also work in some contexts.
# Yes\nmessage = f\"Welcome {name}!\"\n\n# No\nmessage = \"Welcome, {}!\".format(name)\nmessage = \"Welcome, %s!\" % name\nmessage = \"Welcome, \" + name + \"!\"\n\n# Okayish\nwith_ext = name + \".txt\"\n
textwrap.dedent()
to build nice-looking multi-lines strings:# Yes\ndef foo():\n long_message = textwrap.dedent(\"\"\"\\\n first line\n second line\n third line\"\"\")\n\n# No\ndef foo():\n long_message = \"\"\"\\\nfirst line\nsecond line\nthird line\n\"\"\"\n
# Yes\nok, mess = run_command()\n\nfor test_result in test_results:\n outcome, message = res\n\n# No\nfoo, bar = False, \"\"\n\nclass Foo:\n self.bar, self.baz = None, True\n
# Yes\nif foo:\n a = \"ok\"\nelse:\n a = \"nope\"\n\n\n# No:\na = \"ok\" if foo else \"nope\"\n
if ... in ...
when you can:# Yes\nif value in (\"option1\", \"option2\"):\n ...\n\n# No\nif value == \"option1\" or value == \"option2\"\n ...\n
"},{"location":"code-manifesto/#doc_strings_and_comments_in_production_code","title":"Doc strings and comments in production code","text":"First off, bad comments are worse that no comments.
Also note that you should use comments to explain why, never what. If the what is no clear, it means the behavior of the function or method cannot be easily understood by reading implementation, and so you should fix the implementation instead.
In conclusion, use comments and doc strings sparingly: that way, they will not rot and they will stay useful.
Note: this does not apply for tests (see below).
"},{"location":"code-manifesto/#collections","title":"Collections","text":"# Yes\nlist_1.extend(list_2)\n\n# No\nlist_1 += list_2\n
* Only use list()
and dict()
to convert a value to a list or dict. Prefer literals when possible # Yes\nmy_list = []\nmy_dict = {}\n\n# Also yes:\nmy_list = list(yield_stuff())\n\n# No\nmy_list = list()\nmy_dict = dict()\n
# Yes\nmy_copy = list(my_list)\n\n# Also yes:\nmy_copy = copy.copy(my_list)\n\n# No\nmy_copy = my_list[:]\n
# Yes\nmy_list = [foo(x) for x in other_list]\n\n# No\nmy_list = list()\nfor x in other_list:\n x.append(foo(x))\n\n# Also no\nmy_list = map(foo, other_list)\n\n# Yes\neven_nums = [x for x in nums if is_even(x)]\n\n# No\neven_nums = filter(is_even, nums)\n
# Yes\nmax(len(x) for x in my_iterable)\n\n# No\nmax([len(x) for x in my_iterable])\n
for result in results:\n # do something with result\n
"},{"location":"code-manifesto/#functions","title":"Functions","text":"Prefer using keyword-only parameters when possible:
# Yes\n# If the parameter needs a default value:\ndef foo(bar, *, spam=True):\n ...\n\n# If it does not:\ndef foo(bar, *, spam):\n ...\n\n\n# No\ndef foo(bar, spam=True):\n ...\n
If you use the last form, Python will let you use foo(42, False)
, and set spam
to False. This can cause problems if someone ever changes the foo
function and adds a new optional argument before spam
:
def foo(bar, eggs=False, spam=True):\n ...\n
After such a change, the line foo(42, False)
which used to call foo
with spam=False
now calls foo
with bar=False
and spam=True
, leading to all kinds of interesting bugs. Exception to this rule: when the keyword is obvious and will not change:
def get(value, default=None):\n ...\n
"},{"location":"code-manifesto/#imports","title":"Imports","text":"For any foo.py
file, import foo
must never fail, unless there is a necessary module that could not be found. Do not catch ImportError
unless it is necessary, for instance to deal with optional dependencies.
import required_module\n\nHAS_NICE_FEATURE = True\ntry:\n import nice_lib\nexcept ImportError:\n HAS_NICE_FEATURE = False\n\n#...\n\nif HAS_NICE_FEATURE:\n #....\n
Importing Python files should never cause side effects. It's OK to initialize global variables, but you should never call functions outside a if __name__ == main() block
.
Prefer using fully-qualified imports and names:
# Yes\nimport foo.bar\nmy_bar = foo.bar.Bar()\n\n# No\nfrom foo import bar\nmy_bar = bar.Bar()\n
Note
We allow a few exceptions like from pathlib import Path
or importing classes directory in tests. Use your best judgment.
abc.ABCMeta
instead of raising NotImplementedError
. This way you get the error when the class is instantiated instead of when the method is called.# Yes\nclass AbstractFoo(metaclass=abc.ABCMeta):\n @abc.abstractmethod\n def foo(self):\n pass\n\n\n# No\nclass AbstractFoo:\n def foo(self):\n raise NotImplementedError()\n
get_
methods.# Yes\nclass Person:\n def __init__(self, first_name, last_name):\n self.first_name = first_name\n self.last_name = last_name\n\n @property\n def full_name(self):\n return f\"{self.first_name} {self.last_name}\"\n\n\n# No:\nclass Foo:\n def __init__(self, first_name, last_name):\n self.first_name = first_name\n self.last_name = last_name\n self.full_name = f\"{self.first_name} {self.last_name}\"\n
For instance, here:
full_name
is read-onlyfirst_name
changes after the object is initialized.Note that get_
methods are OK if they do more than simple computations (expensive in time or size, throwing exceptions ...)
path.py
library and suffix the variable by _path
. Avoid using os.path
or shutil
methods when path.py
is better.# Yes\nwork_path = Path(\"foo/work\")\nwork_path.mkdir_p()\nfoo_path = work_path / \"foo.txt\"\nfoo_path.write_text(\"this is bar\")\n\n# No\nwork_path = os.path.join(foo, \"work\")\nos.path.mkdir(work_path, exist_ok=True)\nfoo_path = os.path.join(work_path, \"foo.txt\")\nwith open(foo_path, \"w\") as fileobj:\n fileobj.write(\"this is foo\")\n
"},{"location":"code-manifesto/#error_handling","title":"Error handling","text":"tsrc
should derive from tsrc.Error
.Do not use print
, use python-cli-ui functions instead. This makes it easier to distinguish between real messages and the throw-away print
statements you add for debugging.
Also, using \"high-level\" methods such as ui.info_1()
or ui.warning()
will make it easier to have a consistent user interface.
If you think the test implementation is complex, add a human-readable description of the test scenario in the doc string.
For instance:
def test_sync_with_errors(...):\n \"\"\"\" Scenario:\n * Create a manifest with two repos (foo and bar)\n * Initialize a workspace from this manifest\n * Push a new file to the foo repo\n * Create a merge conflict in the foo repo\n * Run `tsrc sync`\n * Check that the command fails and produces the proper error message\n \"\"\"\n
"},{"location":"code-manifesto/#assertions_with_lists","title":"Assertions with lists","text":"# Yes\nactual_list = function_that_returns_list()\n(first, second) = actual_list\nassert first == something\nassert second == something_else\n\n# NO\nactual_list = function_that_returns_list()\nassert len(actual_list) == 2\nfirst = actual_list[0]\nsecond = actual_list[1]\nassert first == something\nassert second == something_else\n
"},{"location":"code-manifesto/#assertion_order","title":"Assertion order","text":"When writing assertions, use the form assert <actual> == <expected>
:
# Yes\ndef test_foo():\n assert foo(42) == True\n\ndef test_big_stuff():\n actual_result = ...\n expected_result = ...\n\n assert actual_result == expected_result\n\n\n# No\ndef test_foo():\n assert True == foo(42)\n\n\ndef test_big_stuff():\n actual_result = ...\n expected_result = ...\n\n assert expected_result == actual_result\n
Rationale:
assert(expected, actual)
convention comes from JUnit but we are not writing Java code, and besides, the assert(actual, expected)
convention also exists in other tools.pytest
does not really care, but we prefer being consistent in all tests.The t
stands for tool
and src
for sources
.
If you speak French, you can also remember the name as \"tes sources\".
"},{"location":"faq/#why_not_repo","title":"Why not repo?","text":"We used repo for a while, but found that tsrc had both a better command line API and a nicer output.
On a less subjective level:
Good support for Windows (no need for Cygwin or anything like that)
Also, tsrc tries hard to never do any destructive operation or unexpected actions.
For instance, tsrc
never puts you in a \"detached HEAD\" state, nor does automatic rebase. It also never touches dirty repositories.
This is achieved by using mostly 'porcelain' commands from git, instead of relying on plumbings internals.
Also (and this matters a lot if you think about contribution):
black
mypy
Note that there are a few features present in repo
that are missing from tsrc
(but may be implemented in the future). Feel free to open a feature request if needed!
All this projects are fine but did not match our needs:
In any case, now that the whole team is using tsrc
all the time, it's likely we'll keep using tsrc
in the future.
It's all about workflow.
With git-submodule
, you have a 'parent' repository and you freeze the state of the 'children' repositories to a specific commit.
It's useful when you want to re-build a library you've forked when you build your main project, or when you have a library or build tools you want to factorize across repositories: this means that each 'parent' repository can have its children on any commit they want.
With tsrc
, all repositories are equal, and what you do instead is to make sure all the branches (or tags) are consistent across repositories.
For instance, if you have foo
and bar
, you are going to make sure the 'master' branch of foo
is always compatible to the 'master' branch of bar
.
Or if you want to go back to the state of the '0.42' release, you will run: tsrc foreach -- git reset --hard v0.42
.
Note that since tsrc 0.2
you can also freeze the commits of some of the repositories.
Last but not least, if you really need to use fixed references, you may do so by adding a sha1
or tag
line to the manifest. See the relevant guide for more details.
First off, we do use pygit2
, but only for tests.
Second, the pygit2
package depends on a 3rd party C library (libgit2
) - and that can cause problems in certain cases. If we can, we prefer using pure-Python libraries for the production code.
Finally, we prefer calling git \"porcelain\" commands, both for readability of the source code and ease of debugging (see below).
"},{"location":"faq/#why_do_you_hide_which_git_commands_are_run","title":"Why do you hide which git commands are run?","text":"It's mainly a matter of not cluttering the output. We take care of keeping the output of tsrc
both concise, readable and informative.
That being said:
--verbose
flag, like so: tsrc --verbose sync
It's nice to read and write, and we use the excellent ruamel.yaml which even has round-trip support.
Also, being Python fans, we don't mind that white space is part of the syntax.
"},{"location":"faq/#why_do_i_have_to_create_a_separate_git_repo_with_just_one_file_in_it","title":"Why do I have to create a separate git repo with just one file in it?","text":"See #235 for why you can't have multiple manifest files in the same repository.
Also, note that you can put other files in the repo - for instance, add a CI script that verifies the yaml syntax and checks that all the repos in the manifest can be cloned.
"},{"location":"getting-started/","title":"Getting started","text":""},{"location":"getting-started/#requirements","title":"Requirements","text":"Python 3.7 or later
"},{"location":"getting-started/#installing_tsrc","title":"Installing tsrc","text":"The recommended way to install tsrc
is to use pipx. This is because pipx
automatically creates isolated environment for each app, so you won't get into dependencies versions conflicts and won't have to deal with manual virtualenvs management.
pip
will also work, but it will not give you these benefits.
Recommended:
pipx install tsrc\n
Acceptable, if you know what you are doing:
pip install tsrc\n
"},{"location":"getting-started/#checking_tsrc_installation","title":"Checking tsrc installation","text":"Run:
$ tsrc --version\n
"},{"location":"getting-started/#creating_a_repository_for_the_manifest","title":"Creating a repository for the manifest","text":"Let's say you are working for the ACME company and you have many git repositories.
You need a tool to track them, so that if a new repository is created, all developers can get a clone on their development machine quickly, without having to look up its URL or even know it exists.
Also, you need to make sure the repos are cloned in a certain way, so that you can for instance refer a repo from an other one by using a relative path.
This is where tsrc
comes in.
The first step is to create a dedicated repository for the manifest. I know it may sound wasteful (\"I have already 100 repositories to manage, and you want me to create yet an other one?\"), but, trust me, it's worth it.
So, if your company uses a GitLab instance at gitlab.acme.com
and you want to crate a manifest for your team, you may start by creating a new repository at https://gitlab.acme.com/your-team/manifest
.
Inside this repository, create a file named manifest.yml
looking like this:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n\n - dest: bar\n url: git@gitlab.acme.com/your-team/bar\n
Note that this approach works with any king of Git Hosting system, not just a custom GitLab instance. Just replace gitlab.acme.com/your-team
with the correct suffix (like github.com/your-name/
if you want to track some repositories from your GitHub account).
Create a new, empty directory and then run tsrc init
from it, using the URL of the manifest created in the above step:
$ mkdir work\n$ cd work\n$ tsrc init git@gitlab.acme.com/your-team/manifest.git\n
You should see something like this:
:: Configuring workspace in /path/to/work\nCloning into 'manifest'...\n...\n=> Cloning missing repos\n* (1/2) Cloning foo\nCloning into 'foo'...\n...\n* (2/2) Cloning bar\nCloning into 'bar'...\n...\n=> Cloned repos:\n* foo cloned from gt@gitlab.acme.com/your-team/foo' (on master)\n* bar cloned from gt@gitlab.acme.com/your-team/bar' (on master)\n=> Configuring remotes\n=> Workspace initialized\n=> Configuration written in /path/to/work/.tsrc/config.yml\n
You will notice that:
foo
ad bar
repositories have been cloned into their respective destination/path/to/work/.tsrc/config.yml
. This file can be edited by hand to customize tsrc
behavior. Follow the relevant guide, or read the workspace configuration file reference for more details.Now let's assume that Alice created a new commit in foo
, and Bob a new commit it bar
, and that they both pushed them to the master
branch of the respective repositories.
Now that you have a workspace configured with tsrc
, you can use tsrc sync
to retrieve all the changes in one go:
$ cd work\n$ tsrc sync\n
This time, you should see the following output:
:: Using workspace in /path/to/work\n=> Updating manifest\n...\n=> Cloning missing repos\n=> Configuring remotes\n=> Synchronizing repos\n* (1/2) Synchronizing foo\n* Fetching origin\n...\n f20af74..aca6c35 master -> origin/master\n* Updating branch: master\nUpdating f20af74..aca6c35\nFast-forward\n new.txt | 1 +\n 1 file changed, 1 insertion(+)\n create mode 100644 new.txt\n* (2/2) Synchronizing bar\n* Fetching origin\n...\n f20af74..02cfef6 master -> origin/master\n* Updating branch: master\nUpdating f20af74..02cfef6\nFast-forward\n spam.py | 1 +\n 1 file changed, 1 insertion(+)\n create mode 100644 spam.py\n:: Workspace synchronized\n
Note: tsrc sync
does not call git pull
on every repository. The precise algorithm is described in the reference documentation
Let's say your team now needs a third repository (for instance, at gitlab.acme.com/your-team/baz
).
Start by making a commit in the manifest
repository that adds the new repository:
--- a/manifest.yml\n+++ b/manifest.yml\n@@ -1,2 +1,3 @@\n repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n\n - dest: bar\n url: git@gitlab.acme.com/your-team/baz\n\n+ - dest: baz\n+ url: git@gitlab.acme.com/your-team/baz\n
Then push this commit to the master
branch of the manifest.
This time when you run tsrc sync
:
manifest
repository will get updatedbaz
repo will be cloned in /path/to/work/baz
$ tsrc sync\n
:: Using workspace in /path/to/work\n=> Updating manifest\nremote: Enumerating objects: 5, done.\n...\nUnpacking objects: 100% (3/3), 354 bytes | 354.00 KiB/s, done.\nFrom gitlab.acme.com/your-team/manifest\n 63f12d4..bbcd4d9 master -> origin/master\nReset branch 'master'\n...\nHEAD is now at bbcd4d9 add baz\n\n=> Cloning missing repos\n* (1/1) Cloning baz\nCloning into 'baz'...\n...\nReceiving objects: 100% (3/3), done.\n=> Cloned repos:\n* baz cloned from git@gitlab.acme.com/bas (on master)\n=> Configuring remotes\n=> Synchronizing repos\n* (1/3) Synchronizing foo\n* Fetching origin\n* Updating branch: master\nAlready up to date.\n* (2/3) Synchronizing bar\n* Fetching origin\n* Updating branch: master\nAlready up to date.\n* (3/3) Synchronizing baz\n* Fetching origin\n* Updating branch: master\nAlready up to date.\n:: Workspace synchronized\n
"},{"location":"getting-started/#going_further","title":"Going further","text":"In this tutorial, we made a lot of assumptions:
master
as the main development branchgitlab.acme.com
in our example)tsrc
can handle all of this use cases, and more. See the other guides for more details.
All the development happens on GitHub.
You are free to open a pull request for anything you want to change on tsrc
.
In particular, pull requests that implement a prototype for a new feature are welcome, having \"real code\" to look at can provide useful insight, even if the code is not merged after all.
That being said, if you want your pull request to be merged, we'll ask that:
See the GitHub actions workflows to see what exactly what commands are run and the Python versions we support.
Also, if relevant, you will need to:
docs/changelog.md
)Finally, feel free to add your name in the THANKS
file ;)
$ poetry install\n
$ poetry run invoke lint\n$ poetry run pytest -n auto\n
"},{"location":"contrib/dev/#adding_documentation","title":"Adding documentation","text":"$ poetry run mkdocs serve\n
docs/
folder and review the changes in your browserReporting bugs and requesting new features is done one the tsrc issue tracker on GitHub.
"},{"location":"contrib/issues/#reporting_bugs","title":"Reporting bugs","text":"If you are reporting a bug, please provide the following information:
tsrc
versionDoing so will ensure we can investigate your bug right away.
"},{"location":"contrib/issues/#suggesting_new_features","title":"Suggesting new features","text":"If you think tsrc
is lacking a feature, please provide the following information:
Note that changingtsrc
behavior can get tricky.
First off, we want to avoid data loss following a tsrc
command above
Second, we want to keep tsrc
behavior as least surprising as possible, so that it can be used without having to read (too much of) documentation.
To that end, and keeping in mind tsrc
needs to accommodate a large variety of use cases, we want to keep the code:
The best way to achieve all of this is to keep it simple.
This means we'll be very cautious before implementing a new feature, so don't hesitate to open an issue for discussion before jumping into the development of a new feature.
"},{"location":"guide/ci/","title":"Using tsrc with Continuous Integration (CI)","text":""},{"location":"guide/ci/#github_actions","title":"GitHub Actions","text":"Let suppose you have a private GitHub organization holding several private repositories and tsrc to synchronize them using the SSH protocol. Let suppose you want to use GitHub Actions to download the code source of your organization, compile it and run some non regression tests. What to write to achieve this with tsrc?
"},{"location":"guide/ci/#step_1_your_tsrc_manifest","title":"Step 1: Your tsrc manifest","text":"Your tsrc manifest.yml
looks something like this:
repos:\n - url: git@github.com:project1/foo\n dest: foo\n
The git@
means SSH protocol.
In your private GitHub repository holding the GitHub workflows files, create the folder .github/workflows
and your yaml file with the desired name and the following content. For more information about GitHub actions syntax see this video:
name: tsrc with private github repos\non:\n workflow_dispatch:\n branches:\n - main\n\njobs:\n export_linux:\n runs-on: ubuntu-latest\n steps:\n - name: Installing tsrc tool\n run: |\n sudo apt-get update\n sudo apt-get install -y python3\n python -m pip install tsrc\n\n - name: Cloning private github repos\n run: |\n git config --global url.\"https://${{ secrets.ACCESS_TOKEN }}@github.com/\".insteadOf git@github.com:\n export WORKSPACE=$GITHUB_WORKSPACE/your_project\n mkdir -p $WORKSPACE\n cd $WORKSPACE\n tsrc init git@github.com:yourorganisation/manifest.git\n tsrc sync\n
This script will run on the latest Ubuntu Docker and triggers steps: - The first step named Installing tsrc tool
allows to install python3 and then tsrc. - The second step named Cloning private github repos
creates a folder named your_project
for your workspace and call the initialization and synchronization of your repositories.
The important command is:
git config --global url.\"https://${{ secrets.ACCESS_TOKEN }}@github.com/\".insteadOf git@github.com:\n
which allows to replace the SSH syntax by the HTTPs syntax on your GitHub repository names.
"},{"location":"guide/ci/#step_3_create_the_github_secret","title":"Step 3: Create the GitHub secret","text":"For GitHub organization one member of the team has the responsibility to hold a Personal access tokens
for the organization. Go https://github.com/settings/tokens and click on the button Generate new token
then click on repo
checkbox then click on the button Generate token
.
Now, this token shall be saved into an action secret named ACCESS_TOKEN
inside the GitHub repository holding the GitHub workflows files.
In the menu Actions
of your repository you can trig the workflow. In this example we used workflow_dispatch
to perform manual triggers. So click on the button to start the process. Once this step done with success, you can update your workflow yaml to complete your CI work: compilation of your project, run non regression tests, etc.
By default, tsrc sync
synchronize projects using branches names.
Usually, one would use the same branch name for several git repositories, like this:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n branch: main\n\n - dest: bar\n url: git@gitlab.acme.com/your-team/bar\n branch: main\n
The assumption here is that foo
and bar
evolve \"at the same time\", so when the main
branch of foo
is updated, the main
branch of bar
much change too.
Sometimes though, this will not be the case. For instance, the main
branch of the bar
repo needs a specific, fixed version of foo
in order to work.
One way to solve this is to push a v1.0 tag in the foo
repository, and change the manifest too look like this:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n- branch: main\n+ tag: v1.0\n
"},{"location":"guide/fixed-refs/#using_a_sha1","title":"Using a sha1","text":"An other way is to put the SHA1 of the relevant git commit in the foo
repository in the manifest:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n branch: main\n+ sha1: ad2b68539c78e749a372414165acdf2a1bb68203\n
"},{"location":"guide/fixed-refs/#cloning_repos_using_fixed_refs","title":"Cloning repos using fixed refs","text":"tsrc
will call git clone --branch <tag>
(which is valid)tsrc
will call git clone
, followed by git reset --hard <sha1>
This is because you cannot tell git to use an arbitrary git reference as start branch when cloning (tags are fine, but sha1s are not).
This also explain why you need both branch
and sha1
in the configuration.
Here's what tsrc sync
will do when trying to synchronize a repo configured with a fixed ref:
git fetch --tags --prune
git reset --hard <tag or sha1>
tsrc
comes with a foreach
command that allows you to run the same command for each repo in the workspace.
This can be used for several things. For instance, if you are building an artifact from a group of repositories, you may want to put a tag on each repo that was used to produce it:
$ tsrc foreach git tag v1.2\n
:: Using workspace in /path/to/work\n:: Running `git tag v1.1` on 2 repos\n/path/to/work/foo $ git tag v1.2\n/path/to/work/bar $ git tag v1.2\n/path/to/work/baz $ git tag v1.2\nOK \u2713\n
"},{"location":"guide/foreach/#caveats","title":"Caveats","text":"-
: you need to call foreach
like this:$ tsrc foreach -- some-command --with-option\n
-c
option:$ tsrc foreach -c 'echo $PWD'\n
Note that we need single quotes here to prevent the shell from expanding the PWD
environment variable when tsrc
is run.
The current tsrc
implementation may not contain all the features your organization needs.
The good news is that you can extend tsrc
's feature set by using tsrc foreach
.
Let's take an example, where you have a manifest containing foo
and bar
and both repos are configured to use a master
branch.
Here's what happens if you run tsrc sync
with bar
on the correct branch (master
), and foo
on an incorrect branch (devel
):
$ tsrc sync\n
:: Using workspace in /path/to/work\n=> Updating manifest\n...\n=> Cloning missing repos\n=> Configuring remotes\n=> Synchronizing repos\n* (1/2) Synchronizing foo\n* Fetching origin\n* Updating branch: devel\nUpdating 702f428..2e4fb45\nFast-forward\n...\n* (2/2) Synchronizing bar\n* Fetching origin\n* Updating branch: master\nAlready up to date.\nError: Failed to synchronize the following repos:\n* foo : Current branch: 'devel' does not match expected branch: 'master'\n
If this happens with multiple repos, you may want a command to checkout the correct branch automatically.
Here's one way to do it:
$ tsrc foreach -c 'git checkout $TSRC_PROJECT_MANIFEST_BRANCH'\n
Here we take advantage of the fact that tsrc
sets the TSRC_PROJECT_MANIFEST_BRANCH
environment variable correctly for each repository before running the command.
Here's the whole list:
Variable DescriptionTSRC_WORKSPACE_PATH
Full path of the workspace root TSRC_MANIFEST_BRANCH
Branch of the manifest TSRC_MANIFEST_URL
URL of the manifest TSRC_PROJECT_CLONE_URL
URL used to clone the repo TSRC_PROJECT_DEST
Relative path of the repo in the workspace TSRC_PROJECT_MANIFEST_BRANCH
Branch configured in the manifest for this repo TSRC_PROJECT_REMOTE_<NAME>
URL of the remote named 'NAME' TSRC_PROJECT_STATUS_DIRTY
Set to true
if the project is dirty, otherwise unset TSRC_PROJECT_STATUS_AHEAD
Number of commits ahead of the remote ref TSRC_PROJECT_STATUS_BEHIND
Number of commits behind the remote ref TSRC_PROJECT_STATUS_BRANCH
Current branch of the repo TSRC_PROJECT_STATUS_SHA1
SHA1 of the current branch TSRC_PROJECT_STATUS_STAGED
Number of files that are staged but not committed TSRC_PROJECT_STATUS_NOT_STAGED
Number of files that are changed but not staged TSRC_PROJECT_STATUS_UNTRACKED
Number of files that are untracked You can implement more complex behavior using the environment variables above, for instance:
#!/bin/bash\n# in switch-and-pull\nif [[ \"${TSRC_PROJECT_STATUS_DIRTY}\" = \"true\" ]]; then\n echo Error: project is dirty\n exit 1\nfi\n\ngit switch $TSRC_PROJECT_MANIFEST_BRANCH\ngit pull\n
$ tsrc foreach switch-and-pull\n:: Running `switch-and-pull` on 2 repos\n* (1/2) foo\n/path/to/foo $ switch-and-pull\nSwitched to branch 'master'\nYour branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.\n (use \"git pull\" to update your local branch)\nUpdating 9e7a8e4..5f9bbd4\nFast-forward\n* (2/2) bar\n/path/to/bar $ switch-and-pull\nError: project is dirty\nError: Command failed for 1 repo(s)\n* bar\n
Of course, feel free to use your favorite programming language here :)
"},{"location":"guide/fs/","title":"Performing file system operations","text":""},{"location":"guide/fs/#introduction","title":"Introduction","text":"When using tsrc
, it is assumed that repositories are put in non-overlapping file system hierarchies, like this:
workspace/\n project_1/\n CMakeLists.txt\n foo.cpp\n bar.cpp\n project_2/\n CMakeLists.txt\n spam.cpp\n eggs.cpp\n
Not like that, where project_2
is inside a sub-directory of project_1
:
workspace/\n project_1/\n CMakeLists.txt\n foo.cpp\n bar.cpp\n project_2/\n CMakeLists.txt\n spam.cpp\n eggs.cpp\n
Note
if you really need project_2
to be a sub-directory of project_1
, consider using git submodules instead.
This is usually fine, except when project_1
and project_2
share some common configuration.
For instance, you may want to use clang-format
for both project_1
and project_2
.
One solution is to put the .clang-format
configuration file in a repo named common
and then tell tsrc
to copy it at the root of the workspace:
repos:\n - dest: project_1\n url: git@acme.com:team/project_1\n\n - dest: project_2\n url: git@acme.com:team/project_2\n\n - dest: common\n url: git@acme.com:team/commont\n copy:\n - file: clang-format\n dest: .clang-format\n
$ tsrc sync\n=> Cloning missing repos\n* (1/1) Cloning common\nCloning into 'common'...\n...\n=> Performing filesystem operations\n* (1/1) Copy /path/to/work/common/clang-format -> /path/to/work/.clang-format\n
Notes:
copy
only works with files, not directories.The above method works fine if the file does not change too often - if not, you may want to create a symbolic link instead:
repos:\n - dest: project_1\n url: git@acme.com:team/project_1\n\n - dest: project_2\n url: git@acme.com:team/project_2\n\n - dest: common\n url: git@acme.com:team/commont\n symlink:\n - source: .clang-format\n target: common/clang-format\n
$ tsrc sync\n=> Cloning missing repos\n...\n=> Performing filesystem operations\n* (1/1) Lint /path/to/work/.clang-format -> common/.clang-format\n
Notes:
The source path for a symbolic link is relative to the top-level <workspace>
, whereas each target path is then relative to the associated source. (This path relationship is essentially identical to how ln -s
works on the command line in Unix-like environments.) Multiple symlinks can be specified; each must specify a source and target.
Symlink creation is supported on all operating systems, but creation of NTFS symlinks on Windows requires that the current user have appropriate security policy permission (SeCreateSymbolicLinkPrivilege). By default, only administrators have that privilege set, although newer versions of Windows 10 support a Developer Mode that permits unprivileged accounts to create symlinks. Note that Cygwin running on Windows defaults to creating links via Windows shortcuts, which do not require any special privileges. (Cygwin's symlink behavior can be user controlled with the winsymlinks
setting in the CYGWIN
environment variable.)
Sometimes it can be necessary to create groups of repositories, especially if the number of repositories grows and if you have people in different teams work on different repositories.
"},{"location":"guide/groups/#defining_groups_in_the_manifest","title":"Defining groups in the manifest","text":"The first step is to edit the manifest.yml
file to describe the groups. Here's an example.
repos:\n - {url: git@gitlab.local:acme/one, dest: one}\n - {url: git@gitlab.local:acme/two, dest: two}\n - {url: git@gitlab.local:acme/three, dest: three}\n\ngroups:\n default:\n repos: []\n g1:\n repos:\n - one\n - two\n g2:\n repos:\n - three\n
Here we define a g1
group that contains repositories named one
and two
, and a g2
group that contains the repository named three
.
tsrc init
","text":"If you only need the repositories in the g1
group you can run:
tsrc init git@gitlab.local:acme/manifest --group g1\n
"},{"location":"guide/groups/#filtering_repositories_in_groups_with_regular_expressions","title":"Filtering repositories in groups with regular expressions","text":"You can utilize inclusive regular expression with the -r
-flag and exclusive regular expression with the -i
-flag. This allows you to filter repositories within a group or a set of groups for the given action.
To include all repositories in the group g1 matching \"config\" and excluding \"template\", you can do the following:
tsrc init git@gitlab.local:acme/manifest --group g1 -r config -i template\n
"},{"location":"guide/groups/#updating_workspace_configuration","title":"Updating workspace configuration","text":"Alternatively, you can edit the .tsrc/config.yml
file, like this:
manifest_url: git@gitlab.local:acme/manifest.git\nmanifest_branch: master\nrepo_groups:\n- g1 # <- specify the list of groups to use\n
You can use this technique to change the groups used in a given workspace - the above method using init
only works to create new workspaces.
The config file contains other configuration options, which are described in the workspace configuration documentation.
"},{"location":"guide/manifest/","title":"Editing the manifest safely","text":""},{"location":"guide/manifest/#introduction_when_things_go_wrong","title":"Introduction: when things go wrong","text":"Let's assume you've successfully implemented tsrc
for your organization - now need to make sure to not break anyone's workflow.
Let's see what could go wrong if you make mistakes while editing the manifest, using a branch called broken
for the sake of the example).
First, let's see what happens if you break the YAML syntax:
commit 1633c5a6 (HEAD -> broken, origin/broken)\n\n Break the manifest syntax\n\ndiff --git a/manifest.yml b/manifest.yml\nindex fe74142..068c35e 100644\n--- a/manifest.yml\n+++ b/manifest.yml\n@@ -1,4 +1,4 @@\n-repos:\n+repos\n - url: git@github.com:your-tools/bar.git\n dest: bar\n
After this change is push, anyone using the broken
branch of the manifest will be faced with this kind of error message:
$ tsrc sync\n
=> Updating manifest\nReset branch 'broken'\nYour branch is up to date with 'origin/broken'.\nBranch 'broken' set up to track remote branch 'broken' from 'origin'.\nHEAD is now at 1633c5a Break the manifest syntax\nError: /path/to/work/.tsrc/manifest/manifest.yml: mapping values are\nnot allowed here :\n\n - url: git@gitlab.acme.com:your-team/foo\n ^ (line: 2)\n
Similarly, if you put an invalid URL in the manifest, like this:
commit ccfb902 (HEAD -> broken, origin/broken)\n\n Use invalid URL for bar repo\n\ndiff --git a/manifest.yml b/manifest.yml\nindex fe74142..068c35e 100644\n--- a/manifest.yml\n+++ b/manifest.yml\n@@ -1,4 +1,4 @@\nrepos:\n- - url: git@gitlab.acme.com:your-team/bar\n+ - url: git@gitlab.acme.com:your-team/invalid\n dest: bar\n
Users will get:
$ tsrc sync\n
:: Using workspace in /path/to/work\n=> Updating manifest\n...\nHEAD is now at ccfb902 Use invalid URL\n=> Cloning missing repos\n=> Configuring remotes\n* bar: Update remote origin to new url: (git@acme.com:your-team/invalid.git)\n...\n=> Synchronizing repos\n* (1/2) Synchronizing bar\n* Fetching origin\nERROR: Repository not found.\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\nError: fetch from 'origin' failed\n* (2/2) Synchronizing foo\n ...\nError: Failed to synchronize the following repos:\n* bar : fetch from 'origin' failed\n
This will probably not be a huge problem for you, dear reader, because you know about tsrc's manifest and its syntax.
It will, however, be a problem for people who are just using tsrc
without knowledge of how it is implemented, because those error messages will definitely confuse them.
If you have a file on your machine containing the manifest changes, you can use tsrc apply-manifest
to check those changes against your own workspace:
$ cd /path/to/work\n$ tsrc apply-manifest /path/to/manifest-repo/manifest.yml\n# Check that the changes are OK\n# If so, commit and push manifest changes:\n$ cd path/to/manifest-repo\n$ git commit -a -m \"...\"\n$ git push\n# Now you know that everyone can safely run `tsrc sync`\n
"},{"location":"guide/manifest/#additional_notes","title":"Additional notes","text":"It is not advised to edit the file in .tsrc/manifest/manifest.yml
directly, because tsrc sync
will silently undo any local changes made to this file. This is a known bug, see #279 for details.
It is common to place the manifest repo itself in the manifest - so it's easy to edit or read:
# In acme.com:your-team/manifest - manifest.yml\nrepos:\n - url: git@acme.com:your-team/manifest\n dest: manifest\n\n - url: git@acme.com:your-team/foo\n dest: foo\n\n - url: git@acme.com:your-team/bar\n dest: bar\n
In that case, you would use:
$ tsrc apply-manifest <workspace>/manifest/manifest.yml\n
to check changes before pushing them.
"},{"location":"guide/remotes/","title":"Using several remotes","text":"When you specify a repository in the manifest with just an URL, tsrc
assumes you want a remote named origin:
repos:\n - dest: foo\n url: git@gitlab.acme.com/your-team/foo\n\n - dest: bar\n url: git@gitlab.acme.com/your-team/bar\n
But sometimes you need several remotes. Let's see a few use cases.
"},{"location":"guide/remotes/#mirroring_open-source_projects","title":"Mirroring open-source projects","text":"If you want some repos in your organization to be open source, you may need:
In that case, you can use an alternative syntax:
repos:\n # foo is open source and thus needs two remotes:\n - dest: foo\n - remotes:\n - name: origin\n url: git@gitlab.acme.com/your-team/foo\n - name: github\n url: git@github.com/your-team/foo\n\n # bar is closed source and thus only needs the\n # default, 'origin' remote:\n - dest: bar\n url: gitlab.acme.com/your-team/bar\n
After this change, when running tsrc init
or tsrc sync
, both the origin
and github
remotes will be created in the foo
repo if they don't exist, and both remotes will be fetched when using tsrc sync
.
Sometimes you will need two remotes, because depending the physical location of your developers, they need to use either:
In that case, you can create a manifest looking like this:
repos:\n - dest: foo\n - remotes:\n - name: origin\n url: git@gitlab.local/your-team/foo\n - name: vpn\n url: git@myvpn.com/gitlab/your-team/foo\n\n - dest: bar\n - remotes:\n - name: origin\n url: git@gitlab.local/your-team/bar\n - name: vpn\n url: git@myvpn.com/gitlab/your-team/bar\n
Developers can then use the -r, --singular-remote
option to either use the origin
or vpn
when running tsrc init
(to create a workspace), or tsrc sync
(to synchronize it), depending on their physical location:
# Init the workspace using the 'vpn' remote\n$ tsrc init -r vpn\n# Bring back the computer in the office\n# Synchronize using the 'origin' remote:\n$ tsrc sync -r origin\n
Note
When using this option, tsrc
expects the remote to be present in the manifest for all repositories.
The configuration file created by tsrc init
contains the whole list of available settings, with their default value, and is located at </path/to/workspace/.tsrc/manifest.yml>
.
Note that if you use command-line options when using tsrc init
, those will be written in the .tsrc/config.yml
.
For instance:
tsrc init git@github.com:dmerejkowsky/dummy-manifest\n
generates this file:
manifest_url: git@github.com:dmerejkowsky/dummy-manifest\nmanifest_branch: master\nrepo_groups: []\nshallow_clones: false\nclone_all_repos: false\nsingular_remote:\n
But
tsrc init git@github.com:dmerejkowsky/dummy-manifest --branch main\n
generates this instead:
manifest_url: git@github.com:dmerejkowsky/dummy-manifest\nmanifest_branch: main\nrepo_groups: []\nshallow_clones: false\nclone_all_repos: false\nsingular_remote:\n
"},{"location":"guide/workspace-config/#editing","title":"Editing","text":"You can edit the workspace configuration as you please, for instance if you need to switch the manifest branch.
If you do so, note that your changes will be taken into account next time you run tsrc sync
.
We use the argparse library to parse command line arguments, so the --help
messages are always up-to-date, probably more so than this documentation :)
tsrc
uses the same \"subcommand\" pattern as git does.
Options common to all commands are placed right before the command name.
Options after the command name only apply to this command.
For instance:
$ tsrc --verbose sync\n$ tsrc init MANIFEST_URL\n
"},{"location":"ref/cli/#goodies","title":"Goodies","text":"First, note that like git
, tsrc will walk up the folders hierarchy looking for a .tsrc
folder, which means you can run tsrc commands anywhere in your workspace, not just at the top.
Second, almost all commands run the operation in parallel. For instance, tsrc sync
by default will use as many jobs as the number of CPUs available on the current machine to synchronize the repos in your workspace. If this behavior is not desired, you can specify a greater (or lower) number of jobs using something like tsrc sync -j2
, or disable the parallelism completely with -j1
. You can also set the default number of jobs by using the TSRC_PARALLEL_JOBS
environment variable.
Initializes a new workspace.
MANIFEST_URL should be a git URL containing a valid manifest.yml
file.
The -g,--groups
option can be used to specify a list of groups to use when cloning repositories.
The -r
\"inclusive regular expression\" and -i
\"exclusive regular expression\" options can be combined with the group option to filter for repositories within a group. -r
takes precedence if both options are present.
The -s,--shallow
option can be used to make shallow clone of all repositories.
If you want to add or remove a group in your workspace, you can edit the configuration file in <workspace>/.tsrc/config.yml
The -r,--singular-remote
option can be used to set a fixed remote to use when cloning and syncing the repositories. If this flag is set, the remote from the manifest with the given name will be used for all repos. It is an error if a repo does not have this remote specified.
Runs command --opt1 arg1
in every repository, and report failures at the end.
Note the --
token to separate options for command
from options for tsrc
.
/bin/sh
on Linux or macOS, cmd.exe
on Windows). tsrc log --from FROM [--to TO] Display a summary of all changes since FROM
(should be a tag), to TO
(defaulting to master
).
Note that if no changes are found, the repository will not be displayed at all.
tsrc statusDisplays a summary of the status of your workspace:
--no-correct-branch
flag is NOT set, then the branch is changed to the configured one and then the repository is updated. Otherwise that repository will not be not updated. tsrc version Displays tsrc
version number, along additional data if run from a git clone. tsrc apply-manifest PATH Apply changes from the manifest file located at PATH
. Useful to check changes in the manifest before publishing them to the manifest repository."},{"location":"ref/manifest-config/","title":"Manifest configuration","text":"The manifest configuration must be stored in a file named manifest.yml
, using YAML syntax.
It is always parsed as a mapping. Here's an example:
repos:\n - url: git@gitlab.local:proj1/foo\n dest: foo\n branch: next\n\n - remotes:\n - name: origin\n url: git@gitlab.local:proj1/bar\n - name: upstream\n url: git@github.com:user/bar\n dest: bar\n branch: master\n sha1: ad2b68539c78e749a372414165acdf2a1bb68203\n\n - url: git@gitlab.local:proj1/app\n dest: app\n tag: v0.1\n copy:\n - file: top.cmake\n dest: CMakeLists.txt\n - file: .clangformat\n symlink:\n - source: app/some_file\n target: ../foo/some_file\n
In this example:
proj1/foo
will be cloned into <workspace>/foo
using the next
branch.proj1/bar
will be cloned into <workspace>/bar
using the master
branch, and reset to ad2b68539c78e749a372414165acdf2a1bb68203
.proj1/app
will be cloned into <workspace>/app
using the v0.1
tag,top.cmake
will be copied from proj1/app/top.cmake
to <workspace>/CMakeLists.txt
,.clang-format
will be copied from proj1/app/
to <workspace>/
, and<workspace>/app/some_file
to <workspace>/foo/some_file
.repos
(required): list of repositories to clonegroups
(optional): list of groupsEach repository is also a mapping, containing:
url
if you just need one remote named origin
name
and url
. In that case, the first remote will be used for cloning the repository.dest
(required): relative path of the repository in the workspacebranch
(optional): The branch to use when cloning the repository (defaults to master
)tag
(optional):tsrc init
: Project will be cloned at the provided tag.tsrc sync
: If the project is clean, project will be reset to the given tag, else a warning message will be printed.sha1
(optional):tsrc init
: Project will be cloned, and then reset to the given sha1.tsrc sync
: If the project is clean, project will be reset to the given sha1, else a warning message will be printed.ignore_submodules
(optional, default=false
):tsrc init
: if ignore_submodules
is true
, do not recursively clone submodules.tsrc sync
: if ignore_submodules
is true
, do not initialize or update submodules. to the given sha1, else a warning message will be printed.copy
(optional): A list of mappings with file
and dest
keys.symlink
(optional): A list of mappings with source
and target
keys.See the Using fixed references and the Performing file system operations guides for details about how and why you would use the tag
, sha1
, copy
or symlink
fields.
The groups
section lists the groups by name. Each group should have a repos
field containing a list of repositories (only repositories defined in the repos
section are allowed).
The groups can optionally include other groups, with a includes
field which should be a list of existing group names.
The group named default
, if it exists, will be used to know which repositories to clone when using tsrc init
and the --group
command line argument is not used.
Example:
repos:\n - dest: a\n url: ..\n - dest: b\n url: ..\n - dest: bar\n url: ..\n - dest: baz\n url: ..\n\ngroups:\n default:\n repos: [a, b]\n foo:\n repos: [bar, baz]\n includes: [default]\n
$ tsrc init <manifest_url>\n# Clones a, b\n$ tsrc init <manifest_url> --group foo\n# Clones a, b, bar and baz\n
Note that tsrc init
records the names of the groups it was invoked with, so that tsrc sync
re-uses them later on. This means that if you want to change the groups used, you must re-run tsrc init
with the new group list.
Note
More information about how to use groups is available in the relevant guide.
"},{"location":"ref/sync/","title":"Sync algorithm","text":"You may have noticed that tsrc sync
does not just calls git pull
on every repository.
Here's the algorithm that is used:
git fetch --tags --prune
--no-correct-branch
flag is NOT set, the branch is changed to the configured one.Note that:
git fetch
is always called so that local refs are up-to-datetsrc
will simply print an error and move on to the next repository if the fast-forward merge is not possible. That's because tsrc
cannot guess what the correct action is, so it prefers doing nothing. It's up to the user to run something like git merge
or git rebase
.The workspace configuration lies in <workspace>/.tsrc/config.yml
. It is created by tsrc init
then read by tsrc sync
and other commands. It can be freely edited by hand.
Here's an example:
manifest_url: git@acme.corp:manifest.git\nmanifest_branch: master\nshallow_clones: false\nrepo_groups:\n- default\nclone_all_repos: false\nsingular_remote:\n
manifest_url
: an git URL containing a manifest.yml
filemanifest_branch
: the branch to use when updating the local manifest (e.g, the first step of tsrc sync
)shallow_clones
: whether to use only shallow clones when cloning missing repositoriesrepo_groups
: the list of groups to use - every mentioned group must be present in the manifest.yml
file (see above)clone_all_repos
: whether to ignore groups entirely and clone every repository from the manifest insteadsingular_remote
: if set to <remote-name>
, behaves as if tsrc sync
and tsrc init
were called with --singular-remote <remote-name>
option. See the Using remotes guide for details.