Repository: huggingface/huggingface_hub
Pull request #282 (2021-08-16 14:10:55)

Push to hub/commit with branches
Completes the `revision`-based API by adding branches management to `push_to_hub` and the `commit` context manager.

### Examples of usage:

#### Using `revision` from `Repository.__init__`

```python
project_name = 'org/project'

# Checkout revision
hf_repo = Repository(project_name, clone_from=project_name, revision='test-1')

model = GPT2LMHeadModel.from_pretrained(project_name)
model.save_pretrained(project_name)

hf_repo.push_to_hub(commit_message=f'step {step}')
```

#### Using `git_checkout` from `Repository`

```python
project_name = 'org/project'

# Checkout revision
hf_repo = Repository(project_name, clone_from=project_name)
hf_repo.git_checkout("branch")

model = GPT2LMHeadModel.from_pretrained(project_name)
model.save_pretrained(project_name)

hf_repo.push_to_hub(commit_message=f'step {step}')
```

Note that the `push_to_hub` method interacts in a complex manner:
- The branch must first be checked out
- The files are saved
- Push to hub should be called

#### Using the `Repository.commit` context manager

Therefore, I would instead recommend using the `Repository.commit` context manager as I find it has a better API, as can be seen below. I think the two could eventually be merged into a single `with Repository.push_to_hub`. See details of the API with usage below:

```python
repo = Repository("local_directory", clone_from="Jikiwa/with-commit-1")

with repo.commit("Add files"):
    with open("file.txt", "w+") as f:
        f.write("Nice")

with repo.commit("Add files to brand-new-branch", branch="brand-new-branch"):
    with open("brand-new-file.txt", "w+") as f:
        f.write("Nice Nice")

repo.git_checkout("nice-branch", create_branch_ok=True)

# Will use lastly checked-out branch
with repo.commit("Add files in nice-branch"):
    with open("file.txt", "w+") as f:
        f.write("Nice")
```

A caveat with `git_checkout` is that it may result in a fatal error if some files are added/modified as the changes would be overridden by the checkout.


Patch:
diff --git a/docs/sagemaker/inference.md b/docs/sagemaker/inference.md
index 74f1f95a..8b307037 100644
--- a/docs/sagemaker/inference.md
+++ b/docs/sagemaker/inference.md
@@ -22,11 +22,11 @@ huggingface_model = HuggingFaceModel(...).deploy()
   - [Deploy the model directly after training](#deploy-the-model-directly-after-training)
   - [Deploy the model using `model_data`](#deploy-the-model-using-model_data)
 - [Deploy for inference one of the 10,000+ 🤗 Transformers models available in the 🤗 Hub](#deploy-for-inference-one-of-the-10000-%F0%9F%A4%97-transformers-models-available-in-the-%F0%9F%A4%97-hub)
+- [Run a Batch Transform Job using 🤗 Transformers and Amazon SageMaker](#run-a-batch-transform-job-using-🤗-transformers-and-amazon-sagemaker)
 - [Advanced Features](#advanced-features)
   - [Environment variables](#environment-variables)
   - [Creating a Model artifact model.tar.gz for deployment](#creating-a-model-artifact-modeltargz-for-deployment)
   - [User defined code/modules](#user-defined-codemodules)
-  - [Batch transform](#batch-transform)
 - [Additional Resources](#additional-resources)
 
 
@@ -135,7 +135,7 @@ sess = sagemaker.Session()
 
 There are two ways to deploy your Hugging Face model trained in SageMaker. You can either deploy it after your training is finished, or you can deploy it later, using the `model_data` pointing to your saved model on S3.
 
-- [Deploy a Hugging Face Transformer model from S3 to SageMaker for inference](https://github.com/huggingface/notebooks/blob/master/sagemaker/10_deploy_model_from_s3/deploy_transformer_model_from_s3.ipynb)
+- [Notebook Example](https://github.com/huggingface/notebooks/blob/master/sagemaker/10_deploy_model_from_s3/deploy_transformer_model_from_s3.ipynb)
 
 
 ### Deploy the model directly after training
@@ -216,7 +216,7 @@ predictor.delete_endpoint()
 
 <iframe width="700" height="394" src="https://www.youtube.com/embed/l9QZuazbzWM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
 
-- [Deploy one of the 10 000+ Hugging Face Transformers to Amazon SageMaker for Inference](https://github.com/huggingface/notebooks/blob/master/sagemaker/11_deploy_model_from_hf_hub/deploy_transformer_model_from_hf_hub.ipynb)
+- [Notebook Example](https://github.com/huggingface/notebooks/blob/master/sagemaker/11_deploy_model_from_hf_hub/deploy_transformer_model_from_hf_hub.ipynb)
 
 To deploy a model directly from the 🤗 Hub to SageMaker, we need to define 2 environment variables when creating the HuggingFaceModel. We need to define:
 
@@ -268,6 +268,82 @@ predictor.delete_endpoint()
 
 ---
 
+
+## Run a Batch Transform Job using 🤗 Transformers and Amazon SageMaker
+
+<iframe width="700" height="394" src="https://www.youtube.com/embed/lnTixz0tUBg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
+
+- [Notebook Example](https://github.com/huggingface/notebooks/blob/master/sagemaker/12_batch_transform_inference/sagemaker-notebook.ipynb)
+
+After you train a model, you can use [Amazon SageMaker Batch Transform](https://docs.aws.amazon.com/sagemaker/latest/dg/how-it-works-batch.html) to perform inferences with the model. In Batch Transform you provide your inference data as an S3 URI and SageMaker will take care of downloading it, running the prediction, and uploading the results afterward to S3 again. You can find more documentation for Batch Transform [here](https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html)
+
+>**The Hugging Face Inference DLC currently only supports `.jsonl` for batch transform, due to the complex structure of textual data.** 
+
+_**NOTE**: While preprocessing, you need to make sure that your `inputs` fit the `max_length`of the model._
+
+If you trained the model using the `HuggingFace` estimator, you can invoke `transformer()` method to create a transform job for a model based on the training job.
+
+```python
+batch_job = huggingface_estimator.transformer(
+    instance_count=1,
+    instance_type='ml.p3.2xlarge',
+    strategy='SingleRecord')
+
+
+batch_job.transform(
+    data='s3://s3-uri-to-batch-data',
+    content_type='application/json',    
+    split_type='Line')
+```
+
+For more details about what can be specified here, see [API docs](https://sagemaker.readthedocs.io/en/stable/overview.html#sagemaker-batch-transform).
+
+If you want to run your Batch Transform Job later or with a model from hf.co/models you can do this by creating a `HuggingFaceModel` instance and then using the `transformer()` method.
+
+```python
+from sagemaker.huggingface.model import HuggingFaceModel
+
+# Hub Model configuration. <https://huggingface.co/models>
+hub = {
+	'HF_MODEL_ID':'distilbert-base-uncased-finetuned-sst-2-english',
+	'HF_TASK':'text-classification'
+}
+
+# create Hugging Face Model Class
+huggingface_model = HuggingFaceModel(
+   env=hub, # configuration for loading model from Hub
+   role=role, # iam role with permissions to create an Endpoint
+   transformers_version="4.6", # transformers version used
+   pytorch_version="1.7", # pytorch version used
+       py_version='py36', # python version used
+)
+
+# create Transformer to run our batch job
+batch_job = huggingface_model.transformer(
+    instance_count=1,
+    instance_type='ml.p3.2xlarge',
+    strategy='SingleRecord')
+
+# starts batch transform job and uses s3 data as input
+batch_job.transform(
+    data='s3://sagemaker-s3-demo-test/samples/input.jsonl',
+    content_type='application/json',    
+    split_type='Line')
+```
+
+The `input.jsonl` looks like this:
+
+```jsonl
+{"inputs":"this movie is terrible"}
+{"inputs":"this movie is amazing"}
+{"inputs":"SageMaker is pretty cool"}
+{"inputs":"SageMaker is pretty cool"}
+{"inputs":"this movie is terrible"}
+{"inputs":"this movie is amazing"}
+```
+
+---
+
 ## Advanced Features
 
 ### Environment variables
@@ -402,72 +478,6 @@ def transform_fn(model, input_data, content_type, accept):
 ```
 
 
-### Batch Transform
-
-After you train a model, you can use [Amazon SageMaker Batch Transform](https://docs.aws.amazon.com/sagemaker/latest/dg/how-it-works-batch.html) to perform inferences with the model. In Batch Transform you provide your inference data as a S3 uri and SageMaker will care of downloading it, running the prediction and uploading the results afterwards to S3 again. You can find more documentation for Batch Transform [here](https://docs.aws.amazon.com/sagemaker/latest/dg/batch-transform.html)
-
-If you trained the model using the `HuggingFace` estimator, you can invoke `transformer()` method to create a transform job for a model based on the training job.
-
-```python
-batch_job = huggingface_estimator.transformer(
-    instance_count=1,
-    instance_type='ml.c5.2xlarge',
-    strategy='SingleRecord')
-
-
-batch_job.transform(
-    data='s3://s3-uri-to-batch-data',
-    content_type='application/json',    
-    split_type='Line')
-```
-
-For more details about what can be specified here, see [API docs](https://sagemaker.readthedocs.io/en/stable/overview.html#sagemaker-batch-transform).
-
-If you want to run your Batch Transform Job later or with a model from hf.co/models you can do this by creating a `HuggingFaceModel` instance and then using the `transformer()` method.
-
-```python
-from sagemaker.huggingface.model import HuggingFaceModel
-
-# Hub Model configuration. <https://huggingface.co/models>
-hub = {
-	'HF_MODEL_ID':'distilbert-base-uncased-finetuned-sst-2-english',
-	'HF_TASK':'text-classification'
-}
-
-# create Hugging Face Model Class
-huggingface_model = HuggingFaceModel(
-   env=hub, # configuration for loading model from Hub
-   role=role, # iam role with permissions to create an Endpoint
-   transformers_version="4.6", # transformers version used
-   pytorch_version="1.7", # pytorch version used
-       py_version='py36', # python version used
-)
-
-# create Transformer to run our batch job
-batch_job = huggingface_model.transformer(
-    instance_count=1,
-    instance_type='ml.c5.2xlarge',
-    strategy='SingleRecord')
-
-# starts batch transform job and uses s3 data as input
-batch_job.transform(
-    data='s3://sagemaker-s3-demo-test/samples/input.jsonl',
-    content_type='application/json',    
-    split_type='Line')
-```
-
-The `input.jsonl` looks like this.
-
-```jsonl
-{"inputs":"this movie is terrible"}
-{"inputs":"this movie is amazing"}
-{"inputs":"SageMaker is pretty cool"}
-{"inputs":"SageMaker is pretty cool"}
-{"inputs":"this movie is terrible"}
-{"inputs":"this movie is amazing"}
-```
-
-
 ## Additional Resources
 
 - [Announcement Blog Post](https://huggingface.co/blog/the-partnership-amazon-sagemaker-and-hugging-face)
diff --git a/src/huggingface_hub/commands/user.py b/src/huggingface_hub/commands/user.py
index 1da7debe..1755033a 100644
--- a/src/huggingface_hub/commands/user.py
+++ b/src/huggingface_hub/commands/user.py
@@ -117,6 +117,27 @@ def tabulate(rows: List[List[Union[str, int]]], headers: List[str]) -> str:
     return "\n".join(lines)
 
 
+def currently_setup_credential_helpers(directory=None) -> List[str]:
+    try:
+        output = subprocess.run(
+            "git config --list".split(),
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            encoding="utf-8",
+            check=True,
+            cwd=directory,
+        ).stdout.split("\n")
+
+        current_credential_helpers = []
+        for line in output:
+            if "credential.helper" in line:
+                current_credential_helpers.append(line.split("=")[-1])
+    except subprocess.CalledProcessError as exc:
+        raise EnvironmentError(exc.stderr)
+
+    return current_credential_helpers
+
+
 class BaseUserCommand:
     def __init__(self, args):
         self.args = args
@@ -147,6 +168,16 @@ class LoginCommand(BaseUserCommand):
         HfFolder.save_token(token)
         print("Login successful")
         print("Your token has been saved to", HfFolder.path_token)
+        helpers = currently_setup_credential_helpers()
+
+        if "store" not in helpers:
+            print(
+                ANSI.red(
+                    "Authenticated through git-crendential store but this isn't the helper defined on your machine.\nYou "
+                    "will have to re-authenticate when pushing to the Hugging Face Hub. Run the following command in your "
+                    "terminal to set it as the default\n\ngit config --global credential.helper store"
+                )
+            )
 
 
 class WhoamiCommand(BaseUserCommand):
diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py
index b730bfbc..b1a32e19 100644
--- a/src/huggingface_hub/hf_api.py
+++ b/src/huggingface_hub/hf_api.py
@@ -16,11 +16,12 @@
 
 import os
 import re
+import subprocess
 import sys
 import warnings
 from io import BufferedIOBase, RawIOBase
 from os.path import expanduser
-from typing import BinaryIO, Dict, Iterable, List, Optional, Union
+from typing import BinaryIO, Dict, Iterable, List, Optional, Tuple, Union
 
 import requests
 from requests.exceptions import HTTPError
@@ -224,6 +225,79 @@ class DatasetInfo:
         return r
 
 
+def write_to_credential_store(username: str, password: str):
+    with subprocess.Popen(
+        "git credential-store store".split(),
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    ) as process:
+        input_username = f"username={username.lower()}"
+        input_password = f"password={password}"
+
+        process.stdin.write(
+            f"url={ENDPOINT}\n{input_username}\n{input_password}\n\n".encode("utf-8")
+        )
+        process.stdin.flush()
+
+
+def read_from_credential_store(
+    username=None,
+) -> Tuple[Union[str, None], Union[str, None]]:
+    """
+    Reads the credential store relative to huggingface.co. If no `username` is specified, will read the first
+    entry for huggingface.co, otherwise will read the entry corresponding to the username specified.
+
+    The username returned will be all lowercase.
+    """
+    with subprocess.Popen(
+        "git credential-store get".split(),
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    ) as process:
+        standard_input = f"url={ENDPOINT}\n"
+
+        if username is not None:
+            standard_input += f"username={username.lower()}\n"
+
+        standard_input += "\n"
+
+        process.stdin.write(standard_input.encode("utf-8"))
+        process.stdin.flush()
+        output = process.stdout.read()
+        output = output.decode("utf-8")
+
+    if len(output) == 0:
+        return None, None
+
+    username, password = [line for line in output.split("\n") if len(line) != 0]
+    return username.split("=")[1], password.split("=")[1]
+
+
+def erase_from_credential_store(username=None):
+    """
+    Erases the credential store relative to huggingface.co. If no `username` is specified, will erase the first
+    entry for huggingface.co, otherwise will erase the entry corresponding to the username specified.
+    """
+    with subprocess.Popen(
+        "git credential-store erase".split(),
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    ) as process:
+        standard_input = f"url={ENDPOINT}\n"
+
+        if username is not None:
+            standard_input += f"username={username.lower()}\n"
+
+        standard_input += "\n"
+
+        process.stdin.write(standard_input.encode("utf-8"))
+        print(standard_input)
+        process.stdin.flush()
+
+
 class HfApi:
     def __init__(self, endpoint=None):
         self.endpoint = endpoint if endpoint is not None else ENDPOINT
@@ -240,6 +314,8 @@ class HfApi:
         r = requests.post(path, json={"username": username, "password": password})
         r.raise_for_status()
         d = r.json()
+
+        write_to_credential_store(username, password)
         return d["token"]
 
     def whoami(self, token: str) -> Dict:
@@ -258,6 +334,9 @@ class HfApi:
         """
         Call HF API to log out.
         """
+        username = self.whoami(token)["name"]
+        erase_from_credential_store(username)
+
         path = "{}/api/logout".format(self.endpoint)
         r = requests.post(path, headers={"authorization": "Bearer {}".format(token)})
         r.raise_for_status()
diff --git a/src/huggingface_hub/repository.py b/src/huggingface_hub/repository.py
index cda08e79..be5a0f1e 100644
--- a/src/huggingface_hub/repository.py
+++ b/src/huggingface_hub/repository.py
@@ -112,6 +112,26 @@ def is_git_ignored(filename: Union[str, Path]) -> bool:
     return is_ignored
 
 
+def files_to_be_staged(pattern: str, folder: Union[str, Path]) -> List[str]:
+    try:
+        p = subprocess.run(
+            ["git", "ls-files", "-mo", pattern],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=folder,
+        )
+        if len(p.stdout.strip()):
+            files = p.stdout.strip().split("\n")
+        else:
+            files = []
+    except subprocess.CalledProcessError as exc:
+        raise EnvironmentError(exc.stderr)
+
+    return files
+
+
 def is_tracked_upstream(folder: Union[str, Path]) -> bool:
     """
     Check if the current checked-out branch is tracked upstream.
@@ -300,11 +320,22 @@ class Repository:
                     "If not specifying `clone_from`, you need to pass Repository a valid git clone."
                 )
 
-        # overrides .git config if user and email is provided.
+        if self.huggingface_token is not None and (
+            git_email is None or git_user is None
+        ):
+            user = HfApi().whoami(self.huggingface_token)
+
+            if git_email is None:
+                git_email = user["email"]
+
+            if git_user is None:
+                git_user = user["fullname"]
+
         if git_user is not None or git_email is not None:
             self.git_config_username_and_email(git_user, git_email)
 
         self.lfs_enable_largefiles()
+        self.git_credential_helper_store()
 
         if revision is not None:
             self.git_checkout(revision, create_branch_ok=True)
@@ -501,6 +532,22 @@ class Repository:
         except subprocess.CalledProcessError as exc:
             raise EnvironmentError(exc.stderr)
 
+    def git_credential_helper_store(self):
+        """
+        sets the git credential helper to `store`
+        """
+        try:
+            subprocess.run(
+                ["git", "config", "credential.helper", "store"],
+                stderr=subprocess.PIPE,
+                stdout=subprocess.PIPE,
+                check=True,
+                encoding="utf-8",
+                cwd=self.local_dir,
+            )
+        except subprocess.CalledProcessError as exc:
+            raise EnvironmentError(exc.stderr)
+
     def git_head_hash(self) -> str:
         """
         Get commit sha on top of HEAD.
@@ -661,22 +708,11 @@ class Repository:
         """
         Automatically track large files with git-lfs
         """
-        try:
-            p = subprocess.run(
-                ["git", "ls-files", "-mo", pattern],
-                stderr=subprocess.PIPE,
-                stdout=subprocess.PIPE,
-                check=True,
-                encoding="utf-8",
-                cwd=self.local_dir,
-            )
-            files_to_be_staged = p.stdout.strip().split("\n")
-        except subprocess.CalledProcessError as exc:
-            raise EnvironmentError(exc.stderr)
+        files_to_be_tracked_with_lfs = []
 
         deleted_files = self.list_deleted_files()
 
-        for filename in files_to_be_staged:
+        for filename in files_to_be_staged(pattern, folder=self.local_dir):
             if filename in deleted_files:
                 continue
 
@@ -689,11 +725,12 @@ class Repository:
                 and not is_git_ignored(path_to_file)
             ):
                 self.lfs_track(filename)
+                files_to_be_tracked_with_lfs.append(filename)
 
         # Cleanup the .gitattributes if files were deleted
         self.lfs_untrack(deleted_files)
 
-        return files_to_be_staged
+        return files_to_be_tracked_with_lfs
 
     def git_pull(self, rebase: Optional[bool] = False):
         """
@@ -704,12 +741,15 @@ class Repository:
             args.append("--rebase")
         try:
             with lfs_log_progress():
-                subprocess.run(
+                result = subprocess.run(
                     args,
+                    stderr=subprocess.PIPE,
+                    stdout=subprocess.PIPE,
                     check=True,
                     encoding="utf-8",
                     cwd=self.local_dir,
                 )
+                logger.info(result.stdout)
         except subprocess.CalledProcessError as exc:
             raise EnvironmentError(exc.stderr)
 
@@ -722,20 +762,21 @@ class Repository:
         """
         if auto_lfs_track:
             tracked_files = self.auto_track_large_files(pattern)
-            if len(tracked_files) > 0:
+            if tracked_files:
                 logger.warning(
-                    "Adding files tracked by Git LFS. This may take a bit of time if the files are large."
+                    f"Adding files tracked by Git LFS: {tracked_files}. This may take a bit of time if the files are large."
                 )
 
         try:
-            subprocess.run(
-                ["git", "add", pattern],
+            result = subprocess.run(
+                ["git", "add", "-v", pattern],
                 stderr=subprocess.PIPE,
                 stdout=subprocess.PIPE,
                 check=True,
                 encoding="utf-8",
                 cwd=self.local_dir,
             )
+            logger.info(f"Adding to index:\n{result.stdout}\n")
         except subprocess.CalledProcessError as exc:
             raise EnvironmentError(exc.stderr)
 
@@ -744,14 +785,15 @@ class Repository:
         git commit
         """
         try:
-            subprocess.run(
-                ["git", "commit", "-m", commit_message],
+            result = subprocess.run(
+                ["git", "commit", "-m", commit_message, "-v"],
                 stderr=subprocess.PIPE,
                 stdout=subprocess.PIPE,
                 check=True,
                 encoding="utf-8",
                 cwd=self.local_dir,
             )
+            logger.info(f"Committed:\n{result.stdout}\n")
         except subprocess.CalledProcessError as exc:
             if len(exc.stderr) > 0:
                 raise EnvironmentError(exc.stderr)
@@ -800,7 +842,7 @@ class Repository:
                 encoding="utf-8",
                 cwd=self.local_dir,
             )
-            logger.warning(f"Checked out {revision}.")
+            logger.warning(f"Checked out {revision} from {self.current_branch}.")
             logger.warning(result.stdout)
         except subprocess.CalledProcessError as exc:
             if not create_branch_ok:
@@ -817,28 +859,43 @@ class Repository:
                         cwd=self.local_dir,
                     )
                     logger.warning(
-                        f"{revision} did not exist. Created and checked out branch {revision}."
+                        f"Revision `{revision}` does not exist. Created and checked out branch `{revision}`."
                     )
                     logger.warning(result.stdout)
                 except subprocess.CalledProcessError as exc:
                     raise EnvironmentError(exc.stderr)
 
-    def push_to_hub(self, commit_message="commit files to HF hub") -> str:
+    def push_to_hub(self, commit_message: str = "commit files to HF hub") -> str:
         """
         Helper to add, commit, and push files to remote repository on the HuggingFace Hub.
+        Will automatically track large files (>10MB).
+
         Args:
             commit_message: commit message.
         """
-        self.git_add()
+        self.git_add(auto_lfs_track=True)
         self.git_commit(commit_message)
-        return self.git_push()
+        return self.git_push(upstream=f"origin {self.current_branch}")
 
     @contextmanager
-    def commit(self, commit_message: str, track_large_files: bool = True):
+    def commit(
+        self,
+        commit_message: str,
+        branch: Optional[str] = None,
+        track_large_files: bool = True,
+    ):
         """
         Context manager utility to handle committing to a repository. This automatically tracks large files (>10Mb)
         with git-lfs. Set the `track_large_files` argument to `False` if you wish to ignore that behavior.
 
+        Args:
+            commit_message (`str`):
+                Message to use for the commit.
+            branch (`str`, `optional`):
+                The branch on which the commit will appear. This branch will be checked-out before any operation.
+            track_large_files (`bool`, `optional`, defaults to `True`):
+                Whether to automatically track large files or not. Will do so by default.
+
         Examples:
 
             >>> with Repository("text-files", clone_from="<user>/text-files", use_auth_token=True).commit("My first file :)"):
@@ -852,6 +909,21 @@ class Repository:
 
         """
 
+        files_to_stage = files_to_be_staged(".", folder=self.local_dir)
+
+        if len(files_to_stage):
+            if len(files_to_stage) > 5:
+                files_to_stage = str(files_to_stage[:5])[:-1] + ", ...]"
+
+            logger.error(
+                f"There exists some updated files in the local repository that are not committed: {files_to_stage}. "
+                "This may lead to errors if checking out a branch. "
+                "These files and their modifications will be added to the current commit."
+            )
+
+        if branch is not None:
+            self.git_checkout(branch, create_branch_ok=True)
+
         if is_tracked_upstream(self.local_dir):
             logger.warning("Pulling changes ...")
             self.git_pull(rebase=True)
diff --git a/widgets/src/lib/InferenceWidget/InferenceWidget.svelte b/widgets/src/lib/InferenceWidget/InferenceWidget.svelte
index fa4c3ec8..94c4e900 100644
--- a/widgets/src/lib/InferenceWidget/InferenceWidget.svelte
+++ b/widgets/src/lib/InferenceWidget/InferenceWidget.svelte
@@ -14,6 +14,7 @@
 	import SummarizationWidget from "./widgets/SummarizationWidget/SummarizationWidget.svelte";
 	import TableQuestionAnsweringWidget from "./widgets/TableQuestionAnsweringWidget/TableQuestionAnsweringWidget.svelte";
 	import TextGenerationWidget from "./widgets/TextGenerationWidget/TextGenerationWidget.svelte";
+	import TextToImageWidget from "./widgets/TextToImageWidget/TextToImageWidget.svelte";
 	import TextToSpeechWidget from "./widgets/TextToSpeechWidget/TextToSpeechWidget.svelte";
 	import TokenClassificationWidget from "./widgets/TokenClassificationWidget/TokenClassificationWidget.svelte";
 	import StructuredDataClassificationWidget from "./widgets/StructuredDataClassificationWidget/StructuredDataClassificationWidget.svelte";
@@ -48,6 +49,7 @@
 		"text-classification": FillMaskWidget,
 		"text-generation": TextGenerationWidget,
 		"token-classification": TokenClassificationWidget,
+		"text-to-image": TextToImageWidget,
 		"text-to-speech": TextToSpeechWidget,
 		translation: TextGenerationWidget,
 		"structured-data-classification": StructuredDataClassificationWidget,
diff --git a/widgets/src/lib/InferenceWidget/widgets/TextToImageWidget/TextToImageWidget.svelte b/widgets/src/lib/InferenceWidget/widgets/TextToImageWidget/TextToImageWidget.svelte
new file mode 100644
index 00000000..ee5fc556
--- /dev/null
+++ b/widgets/src/lib/InferenceWidget/widgets/TextToImageWidget/TextToImageWidget.svelte
@@ -0,0 +1,128 @@
+<script>
+	import type { WidgetProps } from "../../shared/types";
+
+	import { onMount } from "svelte";
+	import WidgetQuickInput from "../../shared/WidgetQuickInput/WidgetQuickInput.svelte";
+	import WidgetWrapper from "../../shared/WidgetWrapper/WidgetWrapper.svelte";
+	import {
+		getDemoInputs,
+		getResponse,
+		getSearchParams,
+		updateUrl,
+	} from "../../shared/helpers";
+
+	export let apiToken: WidgetProps["apiToken"];
+	export let apiUrl: WidgetProps["apiUrl"];
+	export let callApiOnMount: WidgetProps["callApiOnMount"];
+	export let model: WidgetProps["model"];
+	export let noTitle: WidgetProps["noTitle"];
+	export let shouldUpdateUrl: WidgetProps["shouldUpdateUrl"];
+
+	let computeTime = "";
+	let error: string = "";
+	let isLoading = false;
+	let modelLoading = {
+		isLoading: false,
+		estimatedTime: 0,
+	};
+	let output = "";
+	let outputJson = "";
+	let text = "";
+
+	onMount(() => {
+		const [textParam] = getSearchParams(["text"]);
+		if (textParam) {
+			text = textParam;
+			getOutput();
+		} else {
+			const [demoText] = getDemoInputs(model, ["text"]);
+			text = (demoText as string) ?? "";
+			if (text && callApiOnMount) {
+				getOutput();
+			}
+		}
+	});
+
+	async function getOutput(withModelLoading = false) {
+		const trimmedText = text.trim();
+
+		if (!trimmedText) {
+			error = "You need to input some text";
+			output = "";
+			return;
+		}
+
+		if (shouldUpdateUrl) {
+			updateUrl({ text: trimmedText });
+		}
+
+		const requestBody = { inputs: trimmedText };
+
+		isLoading = true;
+
+		const res = await getResponse(
+			apiUrl,
+			model.modelId,
+			requestBody,
+			apiToken,
+			parseOutput,
+			withModelLoading
+		);
+
+		isLoading = false;
+		// Reset values
+		computeTime = "";
+		error = "";
+		modelLoading = { isLoading: false, estimatedTime: 0 };
+		output = "";
+		outputJson = "";
+
+		if (res.status === "success") {
+			computeTime = res.computeTime;
+			output = res.output;
+			outputJson = res.outputJson;
+		} else if (res.status === "loading-model") {
+			modelLoading = {
+				isLoading: true,
+				estimatedTime: res.estimatedTime,
+			};
+			getOutput(true);
+		} else if (res.status === "error") {
+			error = res.error || `Error encountered on input "${trimmedText}"`;
+		}
+	}
+
+	function parseOutput(body: unknown): string {
+		if (body && typeof body === "object" && body instanceof Blob) {
+			return URL.createObjectURL(body);
+		}
+		throw new TypeError(
+			"Invalid output: output must be of type object & of instance Blob"
+		);
+	}
+</script>
+
+<WidgetWrapper
+	{apiUrl}
+	{computeTime}
+	{error}
+	{model}
+	{modelLoading}
+	{noTitle}
+	{outputJson}
+>
+	<svelte:fragment slot="top">
+		<form>
+			<WidgetQuickInput
+				bind:value={text}
+				{isLoading}
+				onClickSubmitBtn={() => getOutput()}
+			/>
+		</form>
+	</svelte:fragment>
+	<svelte:fragment slot="bottom">
+		{#if output.length}
+			<img class="mt-4 object-contain" src={output} alt="" />
+		{/if}
+	</svelte:fragment>
+</WidgetWrapper>
diff --git a/widgets/src/routes/index.svelte b/widgets/src/routes/index.svelte
index c03fec90..6c425c49 100644
--- a/widgets/src/routes/index.svelte
+++ b/widgets/src/routes/index.svelte
@@ -136,6 +136,10 @@
 			pipeline_tag: "conversational",
 			widgetData: [{ text: "Hey my name is Julien! How are you?" }],
 		},
+		{
+			modelId: "osanseviero/BigGAN-deep-128",
+			pipeline_tag: "text-to-image",
+		},
 		{
 			modelId: "julien-c/kan-bayashi_csmsc_tacotron2",
 			pipeline_tag: "text-to-speech",


Test patch:
diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py
index ffb941c6..a66ac12b 100644
--- a/tests/test_hf_api.py
+++ b/tests/test_hf_api.py
@@ -29,6 +29,8 @@ from huggingface_hub.hf_api import (
     HfFolder,
     ModelInfo,
     RepoObj,
+    erase_from_credential_store,
+    read_from_credential_store,
     repo_type_and_id_from_hf_id,
 )
 from requests.exceptions import HTTPError
@@ -67,6 +69,13 @@ class HfApiCommonTest(unittest.TestCase):
 
 
 class HfApiLoginTest(HfApiCommonTest):
+    def setUp(self) -> None:
+        erase_from_credential_store(USER)
+
+    @classmethod
+    def tearDownClass(cls) -> None:
+        cls._api.login(username=USER, password=PASS)
+
     def test_login_invalid(self):
         with self.assertRaises(HTTPError):
             self._api.login(username=USER, password="fake")
@@ -75,6 +84,13 @@ class HfApiLoginTest(HfApiCommonTest):
         token = self._api.login(username=USER, password=PASS)
         self.assertIsInstance(token, str)
 
+    def test_login_git_credentials(self):
+        self.assertTupleEqual(read_from_credential_store(USER), (None, None))
+        self._api.login(username=USER, password=PASS)
+        self.assertTupleEqual(read_from_credential_store(USER), (USER.lower(), PASS))
+        erase_from_credential_store(username=USER)
+        self.assertTupleEqual(read_from_credential_store(USER), (None, None))
+
 
 class HfApiCommonTestWithLogin(HfApiCommonTest):
     @classmethod
diff --git a/tests/test_repository.py b/tests/test_repository.py
index 0b78b091..b1f8c1f5 100644
--- a/tests/test_repository.py
+++ b/tests/test_repository.py
@@ -22,6 +22,7 @@ import unittest
 from io import BytesIO
 
 import requests
+from huggingface_hub.commands.user import currently_setup_credential_helpers
 from huggingface_hub.hf_api import HfApi
 from huggingface_hub.repository import (
     Repository,
@@ -75,6 +76,11 @@ class RepositoryTest(RepositoryCommonTest):
         )
 
     def tearDown(self):
+        try:
+            self._api.delete_repo(token=self._token, name=f"{USER}/{REPO_NAME}")
+        except requests.exceptions.HTTPError:
+            pass
+
         try:
             self._api.delete_repo(token=self._token, name=REPO_NAME)
         except requests.exceptions.HTTPError:
@@ -252,7 +258,7 @@ class RepositoryTest(RepositoryCommonTest):
 
     def test_clone_with_endpoint(self):
         clone = Repository(
-            REPO_NAME,
+            f"{WORKING_REPO_DIR}/{REPO_NAME}",
             clone_from=f"{ENDPOINT_STAGING}/valid_org/{REPO_NAME}",
             use_auth_token=self._token,
             git_user="ci",
@@ -265,7 +271,7 @@ class RepositoryTest(RepositoryCommonTest):
             with open("model.bin", "w") as f:
                 f.write("hello")
 
-        shutil.rmtree(REPO_NAME)
+        shutil.rmtree(f"{WORKING_REPO_DIR}/{REPO_NAME}")
 
         Repository(
             f"{WORKING_REPO_DIR}/{REPO_NAME}",
@@ -281,7 +287,7 @@ class RepositoryTest(RepositoryCommonTest):
 
     def test_clone_with_repo_name_and_org(self):
         clone = Repository(
-            REPO_NAME,
+            f"{WORKING_REPO_DIR}/{REPO_NAME}",
             clone_from=f"valid_org/{REPO_NAME}",
             use_auth_token=self._token,
             git_user="ci",
@@ -294,7 +300,7 @@ class RepositoryTest(RepositoryCommonTest):
             with open("model.bin", "w") as f:
                 f.write("hello")
 
-        shutil.rmtree(REPO_NAME)
+        shutil.rmtree(f"{WORKING_REPO_DIR}/{REPO_NAME}")
 
         Repository(
             f"{WORKING_REPO_DIR}/{REPO_NAME}",
@@ -310,7 +316,7 @@ class RepositoryTest(RepositoryCommonTest):
 
     def test_clone_with_repo_name_and_user_namespace(self):
         clone = Repository(
-            REPO_NAME,
+            f"{WORKING_REPO_DIR}/{REPO_NAME}",
             clone_from=f"{USER}/{REPO_NAME}",
             use_auth_token=self._token,
             git_user="ci",
@@ -325,7 +331,7 @@ class RepositoryTest(RepositoryCommonTest):
             with open("model.bin", "w") as f:
                 f.write("hello")
 
-        shutil.rmtree(REPO_NAME)
+        shutil.rmtree(f"{WORKING_REPO_DIR}/{REPO_NAME}")
 
         Repository(
             f"{WORKING_REPO_DIR}/{REPO_NAME}",
@@ -420,7 +426,7 @@ class RepositoryTest(RepositoryCommonTest):
 
     def test_push_errors_on_wrong_checkout(self):
         repo = Repository(
-            REPO_NAME,
+            WORKING_REPO_DIR,
             clone_from=f"{USER}/{REPO_NAME}",
             use_auth_token=self._token,
             git_user="ci",
@@ -448,7 +454,7 @@ class RepositoryTest(RepositoryCommonTest):
 
     def test_commits_on_correct_branch(self):
         repo = Repository(
-            REPO_NAME,
+            WORKING_REPO_DIR,
             clone_from=f"{USER}/{REPO_NAME}",
             use_auth_token=self._token,
             git_user="ci",
@@ -485,6 +491,88 @@ class RepositoryTest(RepositoryCommonTest):
             self.assertFalse("file.txt" in files)
             self.assertTrue("new_file.txt" in files)
 
+    def test_repo_checkout_push(self):
+        repo = Repository(
+            WORKING_REPO_DIR,
+            clone_from=f"{USER}/{REPO_NAME}",
+            use_auth_token=self._token,
+            git_user="ci",
+            git_email="ci@dummy.com",
+        )
+
+        repo.git_checkout("new-branch", create_branch_ok=True)
+        repo.git_checkout("main")
+
+        with open(os.path.join(repo.local_dir, "file.txt"), "w+") as f:
+            f.write("Ok")
+
+        repo.push_to_hub("Commit #1")
+        repo.git_checkout("new-branch", create_branch_ok=True)
+
+        with open(os.path.join(repo.local_dir, "new_file.txt"), "w+") as f:
+            f.write("Ok")
+
+        repo.push_to_hub("Commit #2")
+
+        with tempfile.TemporaryDirectory() as tmp:
+            clone = Repository(
+                tmp,
+                clone_from=f"{USER}/{REPO_NAME}",
+                use_auth_token=self._token,
+                git_user="ci",
+                git_email="ci@dummy.com",
+            )
+            files = os.listdir(clone.local_dir)
+            self.assertTrue("file.txt" in files)
+            self.assertFalse("new_file.txt" in files)
+
+            clone.git_checkout("new-branch")
+            files = os.listdir(clone.local_dir)
+            self.assertFalse("file.txt" in files)
+            self.assertTrue("new_file.txt" in files)
+
+    def test_repo_checkout_commit_context_manager(self):
+        repo = Repository(
+            WORKING_REPO_DIR,
+            clone_from=f"{USER}/{REPO_NAME}",
+            use_auth_token=self._token,
+            git_user="ci",
+            git_email="ci@dummy.com",
+            revision="main",
+        )
+
+        with repo.commit("Commit #1", branch="new-branch"):
+            with open(os.path.join(repo.local_dir, "file.txt"), "w+") as f:
+                f.write("Ok")
+
+        with repo.commit("Commit #2", branch="main"):
+            with open(os.path.join(repo.local_dir, "new_file.txt"), "w+") as f:
+                f.write("Ok")
+
+        # Maintains lastly used branch
+        with repo.commit("Commit #3"):
+            with open(os.path.join(repo.local_dir, "new_file-2.txt"), "w+") as f:
+                f.write("Ok")
+
+        with tempfile.TemporaryDirectory() as tmp:
+            clone = Repository(
+                tmp,
+                clone_from=f"{USER}/{REPO_NAME}",
+                use_auth_token=self._token,
+                git_user="ci",
+                git_email="ci@dummy.com",
+            )
+            files = os.listdir(clone.local_dir)
+            self.assertFalse("file.txt" in files)
+            self.assertTrue("new_file-2.txt" in files)
+            self.assertTrue("new_file.txt" in files)
+
+            clone.git_checkout("new-branch")
+            files = os.listdir(clone.local_dir)
+            self.assertTrue("file.txt" in files)
+            self.assertFalse("new_file.txt" in files)
+            self.assertFalse("new_file-2.txt" in files)
+
 
 class RepositoryOfflineTest(RepositoryCommonTest):
     @classmethod
@@ -815,6 +903,76 @@ class RepositoryOfflineTest(RepositoryCommonTest):
         files = os.listdir(current_head_repo.local_dir)
         self.assertIn("file.txt", files)
 
+    def test_repo_user(self):
+        api = HfApi(endpoint=ENDPOINT_STAGING)
+        token = api.login(USER, PASS)
+
+        repo = Repository(WORKING_REPO_DIR, use_auth_token=token)
+        user = api.whoami(token)
+
+        username = subprocess.run(
+            ["git", "config", "user.name"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=repo.local_dir,
+        ).stdout.strip()
+        email = subprocess.run(
+            ["git", "config", "user.email"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=repo.local_dir,
+        ).stdout.strip()
+
+        self.assertEqual(username, user["fullname"])
+        self.assertEqual(email, user["email"])
+
+    def test_repo_passed_user(self):
+        api = HfApi(endpoint=ENDPOINT_STAGING)
+        token = api.login(USER, PASS)
+        repo = Repository(
+            WORKING_REPO_DIR,
+            git_user="RANDOM_USER",
+            git_email="EMAIL@EMAIL.EMAIL",
+            use_auth_token=token,
+        )
+        username = subprocess.run(
+            ["git", "config", "user.name"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=repo.local_dir,
+        ).stdout.strip()
+        email = subprocess.run(
+            ["git", "config", "user.email"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+            cwd=repo.local_dir,
+        ).stdout.strip()
+
+        self.assertEqual(username, "RANDOM_USER")
+        self.assertEqual(email, "EMAIL@EMAIL.EMAIL")
+
+    def test_correct_helper(self):
+        subprocess.run(
+            ["git", "config", "--global", "credential.helper", "get"],
+            stderr=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            check=True,
+            encoding="utf-8",
+        )
+        repo = Repository(WORKING_REPO_DIR)
+        self.assertListEqual(
+            currently_setup_credential_helpers(repo.local_dir), ["get", "store"]
+        )
+        self.assertEqual(currently_setup_credential_helpers(), ["get"])
+
 
 class RepositoryDatasetTest(RepositoryCommonTest):
     @classmethod