Repository: iterative/dvc

Issue:
dvc run does not include folder in .gitignore
I have a project that has been working properly. It's fundamentally based on pipeline.
Before when I created the pipeline the models folder was added to .gitignore, however now I can't make it ignore the models folder. Here is the pipeline:

```
dvc add data

dvc run --no-exec \
    -f train.dvc \
    -d code/train.py \
    -d data/train \
    -o models \
    -M metrics/train.json \
    python code/train.py

dvc run --no-exec \
    -f eval.dvc \
    -d code/eval.py \
    -d data/test \
    -d models \
    -M metrics/eval.json \
    python code/eval.py
```

now .gitignore contains:
```
/data
```
however I expect /models to be there as it was (as specified by -o)

**Please provide information about your setup**
dvc 0.87.0
Python 3.6.9
ubuntu 10.04




Comments:
@DavidGOrtega The issue here is `--no-exec`. It only creates the dvc-file but doesn't act on it, hence why `models` is not added to `.gitignore` until you actually run it. Could you explain why you need it/why it matters?
@efiop This issue is actually a bit misleading since the issue is not only the inclusion in gitignore but also is not tracked by DVC until you run repro. It matters specially in CI/CD space. 
The workflow is this:
 - setup a dvc pipeline without running repro locally
 - push to repo
 - CI pull and run repro

When CI does dvc pull errors appears (having to use -force) missing caches or models not tracked.
```
ERROR: failed to pull data from the cloud - Checkout failed for following targets:
  models
 Did you forget to fetch?
```
I'm actually reviewing this. I will come back with more info to update this.
@DavidGOrtega thank you for reporting this! Was you able to find any workaround?

It seems like a valid use case which is not fully supported by DVC.

@efiop is there any specific reason not to create outputs with `--no-exec`?
If we need to support this scenario, what would be your suggestion: create outputs or introduce a new option?
@dmpetrov We could totally do that automatically, I'm just trying to undersatnd the reasons behind it and if we really need it. So far the use case is valid, so I think we could proceed with implementing it.

Patch:
diff --git a/dvc/output/base.py b/dvc/output/base.py
--- a/dvc/output/base.py
+++ b/dvc/output/base.py
@@ -209,6 +209,15 @@ def isdir(self):
     def isfile(self):
         return self.remote.isfile(self.path_info)
 
+    def ignore(self):
+        if not self.use_scm_ignore:
+            return
+
+        if self.repo.scm.is_tracked(self.fspath):
+            raise OutputAlreadyTrackedError(self)
+
+        self.repo.scm.ignore(self.fspath)
+
     def save(self):
         if not self.exists:
             raise self.DoesNotExistError(self)
@@ -219,11 +228,7 @@ def save(self):
         if self.is_empty:
             logger.warning("'{}' is empty.".format(self))
 
-        if self.use_scm_ignore:
-            if self.repo.scm.is_tracked(self.fspath):
-                raise OutputAlreadyTrackedError(self)
-
-            self.repo.scm.ignore(self.fspath)
+        self.ignore()
 
         if not self.use_cache:
             self.info = self.save_info()
diff --git a/dvc/repo/run.py b/dvc/repo/run.py
--- a/dvc/repo/run.py
+++ b/dvc/repo/run.py
@@ -67,10 +67,14 @@ def run(self, fname=None, no_exec=False, **kwargs):
     except OutputDuplicationError as exc:
         raise OutputDuplicationError(exc.output, set(exc.stages) - {stage})
 
-    if not no_exec:
+    if no_exec:
+        for out in stage.outs:
+            out.ignore()
+    else:
         stage.run(
             no_commit=kwargs.get("no_commit", False),
             ignore_build_cache=kwargs.get("ignore_build_cache", False),
         )
+
     dvcfile.dump(stage, update_pipeline=True)
     return stage


Test patch:
diff --git a/tests/func/test_run_single_stage.py b/tests/func/test_run_single_stage.py
--- a/tests/func/test_run_single_stage.py
+++ b/tests/func/test_run_single_stage.py
@@ -30,7 +30,7 @@
 from dvc.system import System
 from dvc.utils import file_md5
 from dvc.utils.stage import load_stage_file
-from tests.basic_env import TestDvc
+from tests.basic_env import TestDvc, TestDvcGit
 
 
 class TestRun(TestDvc):
@@ -90,13 +90,17 @@ def test(self):
             )
 
 
-class TestRunNoExec(TestDvc):
+class TestRunNoExec(TestDvcGit):
     def test(self):
         self.dvc.run(
             cmd="python {} {} {}".format(self.CODE, self.FOO, "out"),
+            deps=[self.CODE, self.FOO],
+            outs=["out"],
             no_exec=True,
         )
         self.assertFalse(os.path.exists("out"))
+        with open(".gitignore", "r") as fobj:
+            self.assertEqual(fobj.read(), "/out\n")
 
 
 class TestRunCircularDependency(TestDvc):