Repository: pantsbuild/pants

Issue:
Local publish.jar should succeed without an SCM repo around it
I have a pants project that for reasons I won't go into doesn't have a git (or any other) repo around it. I tried to run `publish.jar` and `--publish-jar-local=/some/folder` and it crashed with the following message:

```
15:26:13 00:23   [publish]
               Waiting for background workers to finish.
15:26:13 00:23   [complete]
               FAILUREException caught: (<type 'exceptions.AttributeError'>)

Exception message: 'NoneType' object has no attribute 'branch_name'
```

The rest of the build ran fine up until the publish step.

According to @jsirois this is expected but not really desirable behavior, so I'm filing this issue 😄 


Discussion:
To repro using the pants repo:
```
$ git archive --prefix 4772-test-repo/ HEAD | tar -x -C /tmp
$ cd /tmp/4772-test-repo/
$ curl -sSL -O https://pantsbuild.github.io/setup/pants
$ chmod +x pants
$ cat << EOF >> pants.ini
> 
> [GLOBAL]
> pants_version: 1.3.0
> EOF
$ ./pants publish.jar --local=/tmp/4772-test-m2 --no-dryrun --no-prompt src/java/org/pantsbuild/args4j::
...
08:45:59 00:02   [jar]
08:45:59 00:02     [create]
08:45:59 00:02   [doc]
08:45:59 00:02     [javadoc]
08:45:59 00:02     [scaladoc]
08:45:59 00:02   [publish]fatal: Not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).

08:45:59 00:02     [jar]
               Waiting for background workers to finish.
08:45:59 00:02   [complete]
               FAILURE
Exception caught: (<type 'exceptions.AttributeError'>)
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/bin/pants", line 11, in <module>
    sys.exit(main())
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/bin/pants_exe.py", line 44, in main
    PantsRunner(exiter).run()
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/bin/pants_runner.py", line 57, in run
    options_bootstrapper=options_bootstrapper)
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/bin/pants_runner.py", line 46, in _run
    return LocalPantsRunner(exiter, args, env, options_bootstrapper=options_bootstrapper).run()
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/bin/local_pants_runner.py", line 37, in run
    self._run()
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/bin/local_pants_runner.py", line 79, in _run
    goal_runner_result = goal_runner.run()
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/bin/goal_runner.py", line 263, in run
    result = self._execute_engine()
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/bin/goal_runner.py", line 252, in _execute_engine
    result = engine.execute(self._context, self._goals)
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/engine/legacy_engine.py", line 26, in execute
    self.attempt(context, goals)
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/engine/round_engine.py", line 224, in attempt
    goal_executor.attempt(explain)
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/engine/round_engine.py", line 47, in attempt
    task.execute()
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/backend/jvm/tasks/jar_publish.py", line 561, in execute
    self.check_clean_master(commit=(not self.dryrun and self.commit))
  File "/home/jsirois/.cache/pants/setup/bootstrap-Linux-x86_64/1.3.0/lib/python2.7/site-packages/pants/task/scm_publish_mixin.py", line 203, in check_clean_master
    .format(self.scm.branch_name))

Exception message: 'NoneType' object has no attribute 'branch_name'

```
This fix will not be the nicest cleanup - really tangled code here. That said, it will have regression tests.

Resolving patch:
diff --git a/src/python/pants/backend/jvm/tasks/jar_publish.py b/src/python/pants/backend/jvm/tasks/jar_publish.py
--- a/src/python/pants/backend/jvm/tasks/jar_publish.py
+++ b/src/python/pants/backend/jvm/tasks/jar_publish.py
@@ -367,7 +367,6 @@ def __init__(self, *args, **kwargs):
 
     self._jvm_options = self.get_options().jvm_options
 
-    self.scm = get_scm()
     self.log = self.context.log
 
     if self.get_options().local:
@@ -400,6 +399,8 @@ def __init__(self, *args, **kwargs):
       self.push_postscript = self.get_options().push_postscript or ''
       self.local_snapshot = False
 
+    self.scm = get_scm() if self.commit else None
+
     self.named_snapshot = self.get_options().named_snapshot
     if self.named_snapshot:
       self.named_snapshot = Namedver.parse(self.named_snapshot)
@@ -407,7 +408,7 @@ def __init__(self, *args, **kwargs):
     self.dryrun = self.get_options().dryrun
     self.transitive = self.get_options().transitive
     self.force = self.get_options().force
-    self.publish_changelog = self.get_options().changelog
+    self.publish_changelog = self.get_options().changelog and self.scm
 
     def parse_jarcoordinate(coordinate):
       components = coordinate.split('#', 1)
@@ -674,7 +675,7 @@ def stage_artifacts(tgt, jar, version, tag, changelog):
       for (org, name), rev in self.overrides.items():
         print('{0}={1}'.format(coordinate(org, name), rev))
 
-    head_sha = self.scm.commit_id
+    head_sha = self.scm.commit_id if self.scm else None
 
     safe_rmtree(self.workdir)
     published = []
diff --git a/src/python/pants/task/scm_publish_mixin.py b/src/python/pants/task/scm_publish_mixin.py
--- a/src/python/pants/task/scm_publish_mixin.py
+++ b/src/python/pants/task/scm_publish_mixin.py
@@ -198,7 +198,7 @@ def check_clean_master(self, commit=False):
       if changed_files:
         raise self.DirtyWorkspaceError('Can only push from a clean branch, found : {}'
                                        .format(' '.join(changed_files)))
-    else:
+    elif self.scm:
       self.log.info('Skipping check for a clean {} branch in test mode.'
                     .format(self.scm.branch_name))
 


Test patch:
diff --git a/tests/python/pants_test/backend/jvm/tasks/test_jar_publish.py b/tests/python/pants_test/backend/jvm/tasks/test_jar_publish.py
--- a/tests/python/pants_test/backend/jvm/tasks/test_jar_publish.py
+++ b/tests/python/pants_test/backend/jvm/tasks/test_jar_publish.py
@@ -35,7 +35,6 @@ def test_smoke_publish(self):
     with temporary_dir() as publish_dir:
       self.set_options(local=publish_dir)
       task = self.create_task(self.context())
-      task.scm = Mock()
       task.execute()
 
   @property
diff --git a/tests/python/pants_test/backend/jvm/tasks/test_jar_publish_integration.py b/tests/python/pants_test/backend/jvm/tasks/test_jar_publish_integration.py
--- a/tests/python/pants_test/backend/jvm/tasks/test_jar_publish_integration.py
+++ b/tests/python/pants_test/backend/jvm/tasks/test_jar_publish_integration.py
@@ -84,7 +84,6 @@ def test_scala_publish(self):
                        'org.pantsbuild.testproject.publish/jvm-example-lib_2.11/publish.properties',
                        'org.pantsbuild.testproject.publish.hello/welcome_2.11/publish.properties'],
                       extra_options=['--doc-scaladoc-skip'],
-                      expected_primary_artifact_count=3,
                       assert_publish_config_contents=True)
 
   def test_java_publish(self):
@@ -109,8 +108,7 @@ def test_protobuf_publish(self):
                       ['org.pantsbuild.testproject.publish.protobuf/protobuf-java/'
                        'publish.properties',
                        'org.pantsbuild.testproject.protobuf/distance/publish.properties'],
-                      extra_options=['--doc-javadoc-skip'],
-                      expected_primary_artifact_count=2)
+                      extra_options=['--doc-javadoc-skip'])
 
   def test_named_snapshot(self):
     name = "abcdef0123456789"
@@ -220,8 +218,7 @@ def test_override_via_address(self):
       assert_publish_config_contents=True)
 
   def publish_test(self, target, artifacts, pushdb_files, extra_options=None, extra_config=None,
-                   extra_env=None, expected_primary_artifact_count=1, success_expected=True,
-                   assert_publish_config_contents=False):
+                   extra_env=None, success_expected=True, assert_publish_config_contents=False):
     """Tests that publishing the given target results in the expected output.
 
     :param target: Target to test.
@@ -229,7 +226,6 @@ def publish_test(self, target, artifacts, pushdb_files, extra_options=None, extr
     :param pushdb_files: list of pushdb files that would be created if this weren't a local publish
     :param extra_options: Extra command-line options to the pants run.
     :param extra_config: Extra pants.ini configuration for the pants run.
-    :param expected_primary_artifact_count: Number of artifacts we expect to be published.
     :param extra_env: Extra environment variables for the pants run.
     :param assert_publish_config_contents: Test the contents of the generated ivy and pom file.
            If set to True, compares the generated ivy.xml and pom files in
diff --git a/tests/python/pants_test/task/test_scm_publish_mixin.py b/tests/python/pants_test/task/test_scm_publish_mixin.py
--- a/tests/python/pants_test/task/test_scm_publish_mixin.py
+++ b/tests/python/pants_test/task/test_scm_publish_mixin.py
@@ -14,10 +14,10 @@
 
 class ScmPublishMixinTest(unittest.TestCase):
   class ScmPublish(ScmPublishMixin):
-    def __init__(self, restrict_push_branches=None, restrict_push_urls=None):
+    def __init__(self, restrict_push_branches=None, restrict_push_urls=None, scm_available=True):
       self._restrict_push_branches = restrict_push_branches
       self._restrict_push_urls = restrict_push_urls
-      self._scm = mock.Mock()
+      self._scm = mock.Mock() if scm_available else None
       self._log = mock.MagicMock()
 
     @property
@@ -40,6 +40,10 @@ def scm(self):
     def log(self):
       return self._log
 
+  def test_check_clean_master_no_scm(self):
+    scm_publish = self.ScmPublish(scm_available=False)
+    scm_publish.check_clean_master(commit=False)
+
   def test_check_clean_master_dry_run_bad_branch(self):
     scm_publish = self.ScmPublish(restrict_push_branches=['bob'])
     scm_publish.scm.branch_name = 'jane'
@@ -77,7 +81,7 @@ def test_check_clean_master_bad_branch(self):
 
   def test_check_clean_master_bad_remote(self):
     scm_publish = self.ScmPublish(restrict_push_urls=['amy'])
-    scm_publish.scm.serveer_url = 'https://amy'
+    scm_publish.scm.server_url = 'https://amy'
     with self.assertRaises(scm_publish.InvalidRemoteError):
       scm_publish.check_clean_master(commit=True)
 
diff --git a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish.hello/welcome_2.11/ivy-0.0.1-SNAPSHOT.xml b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish.hello/welcome_2.11/ivy-0.0.1-SNAPSHOT.xml
--- a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish.hello/welcome_2.11/ivy-0.0.1-SNAPSHOT.xml
+++ b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish.hello/welcome_2.11/ivy-0.0.1-SNAPSHOT.xml
@@ -11,7 +11,6 @@ Licensed under the Apache License, Version 2.0 (see LICENSE).
   <publications>
     <artifact/>
     <artifact m:classifier="sources"/>
-    <artifact m:classifier="CHANGELOG" ext="txt"/>
     <artifact type="pom"/>
   </publications>
 </ivy-module>
diff --git a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/classifiers_2.11/ivy-0.0.1-SNAPSHOT.xml b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/classifiers_2.11/ivy-0.0.1-SNAPSHOT.xml
--- a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/classifiers_2.11/ivy-0.0.1-SNAPSHOT.xml
+++ b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/classifiers_2.11/ivy-0.0.1-SNAPSHOT.xml
@@ -12,7 +12,6 @@ Licensed under the Apache License, Version 2.0 (see LICENSE).
     <artifact/>
     <artifact m:classifier="sources"/>
     <artifact m:classifier="javadoc"/>
-    <artifact m:classifier="CHANGELOG" ext="txt"/>
     <artifact type="pom"/>
   </publications>
 </ivy-module>
diff --git a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/classifiers_2.11/ivy-1.2.3-SNAPSHOT.xml b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/classifiers_2.11/ivy-1.2.3-SNAPSHOT.xml
--- a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/classifiers_2.11/ivy-1.2.3-SNAPSHOT.xml
+++ b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/classifiers_2.11/ivy-1.2.3-SNAPSHOT.xml
@@ -12,7 +12,6 @@ Licensed under the Apache License, Version 2.0 (see LICENSE).
     <artifact/>
     <artifact m:classifier="sources"/>
     <artifact m:classifier="javadoc"/>
-    <artifact m:classifier="CHANGELOG" ext="txt"/>
     <artifact type="pom"/>
   </publications>
 </ivy-module>
diff --git a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/hello-greet/ivy-0.0.1-SNAPSHOT.xml b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/hello-greet/ivy-0.0.1-SNAPSHOT.xml
--- a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/hello-greet/ivy-0.0.1-SNAPSHOT.xml
+++ b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/hello-greet/ivy-0.0.1-SNAPSHOT.xml
@@ -12,7 +12,6 @@ Licensed under the Apache License, Version 2.0 (see LICENSE).
     <artifact/>
     <artifact m:classifier="sources"/>
     <artifact m:classifier="javadoc"/>
-    <artifact m:classifier="CHANGELOG" ext="txt"/>
     <artifact type="pom"/>
   </publications>
 </ivy-module>
diff --git a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/jvm-example-lib_2.11/ivy-0.0.1-SNAPSHOT.xml b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/jvm-example-lib_2.11/ivy-0.0.1-SNAPSHOT.xml
--- a/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/jvm-example-lib_2.11/ivy-0.0.1-SNAPSHOT.xml
+++ b/tests/python/pants_test/tasks/jar_publish_resources/org.pantsbuild.testproject.publish/jvm-example-lib_2.11/ivy-0.0.1-SNAPSHOT.xml
@@ -11,7 +11,6 @@ Licensed under the Apache License, Version 2.0 (see LICENSE).
   <publications>
     <artifact/>
     <artifact m:classifier="sources"/>
-    <artifact m:classifier="CHANGELOG" ext="txt"/>
     <artifact type="pom"/>
   </publications>
 </ivy-module>